diff --git a/coderd/userauth.go b/coderd/userauth.go
index 99ca8dd7598f8..a969e372691bf 100644
--- a/coderd/userauth.go
+++ b/coderd/userauth.go
@@ -889,51 +889,6 @@ func (api *API) userOIDC(rw http.ResponseWriter, r *http.Request) {
 		}
 	}
 
-	var usingGroups bool
-	var groups []string
-	// If the GroupField is the empty string, then groups from OIDC are not used.
-	// This is so we can support manual group assignment.
-	if api.OIDCConfig.GroupField != "" {
-		usingGroups = true
-		groupsRaw, ok := mergedClaims[api.OIDCConfig.GroupField]
-		if ok && api.OIDCConfig.GroupField != "" {
-			// Convert the []interface{} we get to a []string.
-			groupsInterface, ok := groupsRaw.([]interface{})
-			if ok {
-				api.Logger.Debug(ctx, "groups returned in oidc claims",
-					slog.F("len", len(groupsInterface)),
-					slog.F("groups", groupsInterface),
-				)
-
-				for _, groupInterface := range groupsInterface {
-					group, ok := groupInterface.(string)
-					if !ok {
-						httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
-							Message: fmt.Sprintf("Invalid group type. Expected string, got: %T", groupInterface),
-						})
-						return
-					}
-
-					if mappedGroup, ok := api.OIDCConfig.GroupMapping[group]; ok {
-						group = mappedGroup
-					}
-
-					groups = append(groups, group)
-				}
-			} else {
-				api.Logger.Debug(ctx, "groups field was an unknown type",
-					slog.F("type", fmt.Sprintf("%T", groupsRaw)),
-				)
-			}
-		}
-	}
-
-	// This conditional is purely to warn the user they might have misconfigured their OIDC
-	// configuration.
-	if _, groupClaimExists := mergedClaims["groups"]; !usingGroups && groupClaimExists {
-		logger.Debug(ctx, "claim 'groups' was returned, but 'oidc-group-field' is not set, check your coder oidc settings")
-	}
-
 	// The username is a required property in Coder. We make a best-effort
 	// attempt at using what the claims provide, but if that fails we will
 	// generate a random username.
@@ -970,6 +925,21 @@ func (api *API) userOIDC(rw http.ResponseWriter, r *http.Request) {
 		picture, _ = pictureRaw.(string)
 	}
 
+	usingGroups, groups, err := api.oidcGroups(ctx, mergedClaims)
+	if err != nil {
+		httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
+			Message: "Failed to sync groups from OIDC claims",
+			Detail:  err.Error(),
+		})
+		return
+	}
+
+	roles, ok := api.oidcRoles(ctx, rw, r, mergedClaims)
+	if !ok {
+		// oidcRoles writes the error to the response writer for us.
+		return
+	}
+
 	user, link, err := findLinkedUser(ctx, api.Database, oidcLinkedID(idToken), email)
 	if err != nil {
 		logger.Error(ctx, "oauth2: unable to find linked user", slog.F("email", email), slog.Error(err))
@@ -980,63 +950,6 @@ func (api *API) userOIDC(rw http.ResponseWriter, r *http.Request) {
 		return
 	}
 
-	roles := api.OIDCConfig.UserRolesDefault
-	if api.OIDCConfig.RoleSyncEnabled() {
-		rolesRow, ok := mergedClaims[api.OIDCConfig.UserRoleField]
-		if !ok {
-			// If no claim is provided than we can assume the user is just
-			// a member. This is because there is no way to tell the difference
-			// between []string{} and nil for OIDC claims. IDPs omit claims
-			// if they are empty ([]string{}).
-			// Use []interface{}{} so the next typecast works.
-			rolesRow = []interface{}{}
-		}
-
-		rolesInterface, ok := rolesRow.([]interface{})
-		if !ok {
-			api.Logger.Error(ctx, "oidc claim user roles field was an unknown type",
-				slog.F("type", fmt.Sprintf("%T", rolesRow)),
-			)
-			site.RenderStaticErrorPage(rw, r, site.ErrorPageData{
-				Status:       http.StatusInternalServerError,
-				HideStatus:   true,
-				Title:        "Login disabled until OIDC config is fixed",
-				Description:  fmt.Sprintf("Roles claim must be an array of strings, type found: %T. Disabling role sync will allow login to proceed.", rolesRow),
-				RetryEnabled: false,
-				DashboardURL: "/login",
-			})
-			return
-		}
-
-		api.Logger.Debug(ctx, "roles returned in oidc claims",
-			slog.F("len", len(rolesInterface)),
-			slog.F("roles", rolesInterface),
-		)
-		for _, roleInterface := range rolesInterface {
-			role, ok := roleInterface.(string)
-			if !ok {
-				api.Logger.Error(ctx, "invalid oidc user role type",
-					slog.F("type", fmt.Sprintf("%T", rolesRow)),
-				)
-				httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
-					Message: fmt.Sprintf("Invalid user role type. Expected string, got: %T", roleInterface),
-				})
-				return
-			}
-
-			if mappedRoles, ok := api.OIDCConfig.UserRoleMapping[role]; ok {
-				if len(mappedRoles) == 0 {
-					continue
-				}
-				// Mapped roles are added to the list of roles
-				roles = append(roles, mappedRoles...)
-				continue
-			}
-
-			roles = append(roles, role)
-		}
-	}
-
 	// If a new user is authenticating for the first time
 	// the audit action is 'register', not 'login'
 	if user.ID == uuid.Nil {
@@ -1053,9 +966,9 @@ func (api *API) userOIDC(rw http.ResponseWriter, r *http.Request) {
 		Email:               email,
 		Username:            username,
 		AvatarURL:           picture,
-		UsingGroups:         usingGroups,
 		UsingRoles:          api.OIDCConfig.RoleSyncEnabled(),
 		Roles:               roles,
+		UsingGroups:         usingGroups,
 		Groups:              groups,
 		CreateMissingGroups: api.OIDCConfig.CreateMissingGroups,
 		GroupFilter:         api.OIDCConfig.GroupFilter,
@@ -1095,6 +1008,123 @@ func (api *API) userOIDC(rw http.ResponseWriter, r *http.Request) {
 	http.Redirect(rw, r, redirect, http.StatusTemporaryRedirect)
 }
 
+// oidcGroups returns the groups for the user from the OIDC claims.
+func (api *API) oidcGroups(ctx context.Context, mergedClaims map[string]interface{}) (bool, []string, error) {
+	logger := api.Logger.Named(userAuthLoggerName)
+	usingGroups := false
+	var groups []string
+
+	// If the GroupField is the empty string, then groups from OIDC are not used.
+	// This is so we can support manual group assignment.
+	if api.OIDCConfig.GroupField != "" {
+		usingGroups = true
+		groupsRaw, ok := mergedClaims[api.OIDCConfig.GroupField]
+		if ok && api.OIDCConfig.GroupField != "" {
+			// Convert the []interface{} we get to a []string.
+			groupsInterface, ok := groupsRaw.([]interface{})
+			if ok {
+				api.Logger.Debug(ctx, "groups returned in oidc claims",
+					slog.F("len", len(groupsInterface)),
+					slog.F("groups", groupsInterface),
+				)
+
+				for _, groupInterface := range groupsInterface {
+					group, ok := groupInterface.(string)
+					if !ok {
+						return false, nil, xerrors.Errorf("Invalid group type. Expected string, got: %T", groupInterface)
+					}
+
+					if mappedGroup, ok := api.OIDCConfig.GroupMapping[group]; ok {
+						group = mappedGroup
+					}
+
+					groups = append(groups, group)
+				}
+			} else {
+				api.Logger.Debug(ctx, "groups field was an unknown type",
+					slog.F("type", fmt.Sprintf("%T", groupsRaw)),
+				)
+			}
+		}
+	}
+
+	// This conditional is purely to warn the user they might have misconfigured their OIDC
+	// configuration.
+	if _, groupClaimExists := mergedClaims["groups"]; !usingGroups && groupClaimExists {
+		logger.Debug(ctx, "claim 'groups' was returned, but 'oidc-group-field' is not set, check your coder oidc settings")
+	}
+
+	return usingGroups, groups, nil
+}
+
+// oidcRoles returns the roles for the user from the OIDC claims.
+// If the function returns false, then the caller should return early.
+// All writes to the response writer are handled by this function.
+// It would be preferred to just return an error, however this function
+// decorates returned errors with the appropriate HTTP status codes and details
+// that are hard to carry in a standard `error` without more work.
+func (api *API) oidcRoles(ctx context.Context, rw http.ResponseWriter, r *http.Request, mergedClaims map[string]interface{}) ([]string, bool) {
+	roles := api.OIDCConfig.UserRolesDefault
+	if !api.OIDCConfig.RoleSyncEnabled() {
+		return roles, true
+	}
+
+	rolesRow, ok := mergedClaims[api.OIDCConfig.UserRoleField]
+	if !ok {
+		// If no claim is provided than we can assume the user is just
+		// a member. This is because there is no way to tell the difference
+		// between []string{} and nil for OIDC claims. IDPs omit claims
+		// if they are empty ([]string{}).
+		// Use []interface{}{} so the next typecast works.
+		rolesRow = []interface{}{}
+	}
+
+	rolesInterface, ok := rolesRow.([]interface{})
+	if !ok {
+		api.Logger.Error(ctx, "oidc claim user roles field was an unknown type",
+			slog.F("type", fmt.Sprintf("%T", rolesRow)),
+		)
+		site.RenderStaticErrorPage(rw, r, site.ErrorPageData{
+			Status:       http.StatusInternalServerError,
+			HideStatus:   true,
+			Title:        "Login disabled until OIDC config is fixed",
+			Description:  fmt.Sprintf("Roles claim must be an array of strings, type found: %T. Disabling role sync will allow login to proceed.", rolesRow),
+			RetryEnabled: false,
+			DashboardURL: "/login",
+		})
+		return nil, false
+	}
+
+	api.Logger.Debug(ctx, "roles returned in oidc claims",
+		slog.F("len", len(rolesInterface)),
+		slog.F("roles", rolesInterface),
+	)
+	for _, roleInterface := range rolesInterface {
+		role, ok := roleInterface.(string)
+		if !ok {
+			api.Logger.Error(ctx, "invalid oidc user role type",
+				slog.F("type", fmt.Sprintf("%T", rolesRow)),
+			)
+			httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
+				Message: fmt.Sprintf("Invalid user role type. Expected string, got: %T", roleInterface),
+			})
+			return nil, false
+		}
+
+		if mappedRoles, ok := api.OIDCConfig.UserRoleMapping[role]; ok {
+			if len(mappedRoles) == 0 {
+				continue
+			}
+			// Mapped roles are added to the list of roles
+			roles = append(roles, mappedRoles...)
+			continue
+		}
+
+		roles = append(roles, role)
+	}
+	return roles, true
+}
+
 // claimFields returns the sorted list of fields in the claims map.
 func claimFields(claims map[string]interface{}) []string {
 	fields := []string{}