@@ -889,51 +889,6 @@ func (api *API) userOIDC(rw http.ResponseWriter, r *http.Request) {
889
889
}
890
890
}
891
891
892
- var usingGroups bool
893
- var groups []string
894
- // If the GroupField is the empty string, then groups from OIDC are not used.
895
- // This is so we can support manual group assignment.
896
- if api .OIDCConfig .GroupField != "" {
897
- usingGroups = true
898
- groupsRaw , ok := mergedClaims [api .OIDCConfig .GroupField ]
899
- if ok && api .OIDCConfig .GroupField != "" {
900
- // Convert the []interface{} we get to a []string.
901
- groupsInterface , ok := groupsRaw .([]interface {})
902
- if ok {
903
- api .Logger .Debug (ctx , "groups returned in oidc claims" ,
904
- slog .F ("len" , len (groupsInterface )),
905
- slog .F ("groups" , groupsInterface ),
906
- )
907
-
908
- for _ , groupInterface := range groupsInterface {
909
- group , ok := groupInterface .(string )
910
- if ! ok {
911
- httpapi .Write (ctx , rw , http .StatusBadRequest , codersdk.Response {
912
- Message : fmt .Sprintf ("Invalid group type. Expected string, got: %T" , groupInterface ),
913
- })
914
- return
915
- }
916
-
917
- if mappedGroup , ok := api .OIDCConfig .GroupMapping [group ]; ok {
918
- group = mappedGroup
919
- }
920
-
921
- groups = append (groups , group )
922
- }
923
- } else {
924
- api .Logger .Debug (ctx , "groups field was an unknown type" ,
925
- slog .F ("type" , fmt .Sprintf ("%T" , groupsRaw )),
926
- )
927
- }
928
- }
929
- }
930
-
931
- // This conditional is purely to warn the user they might have misconfigured their OIDC
932
- // configuration.
933
- if _ , groupClaimExists := mergedClaims ["groups" ]; ! usingGroups && groupClaimExists {
934
- logger .Debug (ctx , "claim 'groups' was returned, but 'oidc-group-field' is not set, check your coder oidc settings" )
935
- }
936
-
937
892
// The username is a required property in Coder. We make a best-effort
938
893
// attempt at using what the claims provide, but if that fails we will
939
894
// generate a random username.
@@ -970,6 +925,21 @@ func (api *API) userOIDC(rw http.ResponseWriter, r *http.Request) {
970
925
picture , _ = pictureRaw .(string )
971
926
}
972
927
928
+ usingGroups , groups , err := api .oidcGroups (ctx , mergedClaims )
929
+ if err != nil {
930
+ httpapi .Write (ctx , rw , http .StatusBadRequest , codersdk.Response {
931
+ Message : "Failed to sync groups from OIDC claims" ,
932
+ Detail : err .Error (),
933
+ })
934
+ return
935
+ }
936
+
937
+ roles , ok := api .oidcRoles (ctx , rw , r , mergedClaims )
938
+ if ! ok {
939
+ // oidcRoles writes the error to the response writer for us.
940
+ return
941
+ }
942
+
973
943
user , link , err := findLinkedUser (ctx , api .Database , oidcLinkedID (idToken ), email )
974
944
if err != nil {
975
945
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) {
980
950
return
981
951
}
982
952
983
- roles := api .OIDCConfig .UserRolesDefault
984
- if api .OIDCConfig .RoleSyncEnabled () {
985
- rolesRow , ok := mergedClaims [api .OIDCConfig .UserRoleField ]
986
- if ! ok {
987
- // If no claim is provided than we can assume the user is just
988
- // a member. This is because there is no way to tell the difference
989
- // between []string{} and nil for OIDC claims. IDPs omit claims
990
- // if they are empty ([]string{}).
991
- // Use []interface{}{} so the next typecast works.
992
- rolesRow = []interface {}{}
993
- }
994
-
995
- rolesInterface , ok := rolesRow .([]interface {})
996
- if ! ok {
997
- api .Logger .Error (ctx , "oidc claim user roles field was an unknown type" ,
998
- slog .F ("type" , fmt .Sprintf ("%T" , rolesRow )),
999
- )
1000
- site .RenderStaticErrorPage (rw , r , site.ErrorPageData {
1001
- Status : http .StatusInternalServerError ,
1002
- HideStatus : true ,
1003
- Title : "Login disabled until OIDC config is fixed" ,
1004
- Description : fmt .Sprintf ("Roles claim must be an array of strings, type found: %T. Disabling role sync will allow login to proceed." , rolesRow ),
1005
- RetryEnabled : false ,
1006
- DashboardURL : "/login" ,
1007
- })
1008
- return
1009
- }
1010
-
1011
- api .Logger .Debug (ctx , "roles returned in oidc claims" ,
1012
- slog .F ("len" , len (rolesInterface )),
1013
- slog .F ("roles" , rolesInterface ),
1014
- )
1015
- for _ , roleInterface := range rolesInterface {
1016
- role , ok := roleInterface .(string )
1017
- if ! ok {
1018
- api .Logger .Error (ctx , "invalid oidc user role type" ,
1019
- slog .F ("type" , fmt .Sprintf ("%T" , rolesRow )),
1020
- )
1021
- httpapi .Write (ctx , rw , http .StatusBadRequest , codersdk.Response {
1022
- Message : fmt .Sprintf ("Invalid user role type. Expected string, got: %T" , roleInterface ),
1023
- })
1024
- return
1025
- }
1026
-
1027
- if mappedRoles , ok := api .OIDCConfig .UserRoleMapping [role ]; ok {
1028
- if len (mappedRoles ) == 0 {
1029
- continue
1030
- }
1031
- // Mapped roles are added to the list of roles
1032
- roles = append (roles , mappedRoles ... )
1033
- continue
1034
- }
1035
-
1036
- roles = append (roles , role )
1037
- }
1038
- }
1039
-
1040
953
// If a new user is authenticating for the first time
1041
954
// the audit action is 'register', not 'login'
1042
955
if user .ID == uuid .Nil {
@@ -1053,9 +966,9 @@ func (api *API) userOIDC(rw http.ResponseWriter, r *http.Request) {
1053
966
Email : email ,
1054
967
Username : username ,
1055
968
AvatarURL : picture ,
1056
- UsingGroups : usingGroups ,
1057
969
UsingRoles : api .OIDCConfig .RoleSyncEnabled (),
1058
970
Roles : roles ,
971
+ UsingGroups : usingGroups ,
1059
972
Groups : groups ,
1060
973
CreateMissingGroups : api .OIDCConfig .CreateMissingGroups ,
1061
974
GroupFilter : api .OIDCConfig .GroupFilter ,
@@ -1095,6 +1008,123 @@ func (api *API) userOIDC(rw http.ResponseWriter, r *http.Request) {
1095
1008
http .Redirect (rw , r , redirect , http .StatusTemporaryRedirect )
1096
1009
}
1097
1010
1011
+ // oidcGroups returns the groups for the user from the OIDC claims.
1012
+ func (api * API ) oidcGroups (ctx context.Context , mergedClaims map [string ]interface {}) (bool , []string , error ) {
1013
+ logger := api .Logger .Named (userAuthLoggerName )
1014
+ usingGroups := false
1015
+ var groups []string
1016
+
1017
+ // If the GroupField is the empty string, then groups from OIDC are not used.
1018
+ // This is so we can support manual group assignment.
1019
+ if api .OIDCConfig .GroupField != "" {
1020
+ usingGroups = true
1021
+ groupsRaw , ok := mergedClaims [api .OIDCConfig .GroupField ]
1022
+ if ok && api .OIDCConfig .GroupField != "" {
1023
+ // Convert the []interface{} we get to a []string.
1024
+ groupsInterface , ok := groupsRaw .([]interface {})
1025
+ if ok {
1026
+ api .Logger .Debug (ctx , "groups returned in oidc claims" ,
1027
+ slog .F ("len" , len (groupsInterface )),
1028
+ slog .F ("groups" , groupsInterface ),
1029
+ )
1030
+
1031
+ for _ , groupInterface := range groupsInterface {
1032
+ group , ok := groupInterface .(string )
1033
+ if ! ok {
1034
+ return false , nil , xerrors .Errorf ("Invalid group type. Expected string, got: %T" , groupInterface )
1035
+ }
1036
+
1037
+ if mappedGroup , ok := api .OIDCConfig .GroupMapping [group ]; ok {
1038
+ group = mappedGroup
1039
+ }
1040
+
1041
+ groups = append (groups , group )
1042
+ }
1043
+ } else {
1044
+ api .Logger .Debug (ctx , "groups field was an unknown type" ,
1045
+ slog .F ("type" , fmt .Sprintf ("%T" , groupsRaw )),
1046
+ )
1047
+ }
1048
+ }
1049
+ }
1050
+
1051
+ // This conditional is purely to warn the user they might have misconfigured their OIDC
1052
+ // configuration.
1053
+ if _ , groupClaimExists := mergedClaims ["groups" ]; ! usingGroups && groupClaimExists {
1054
+ logger .Debug (ctx , "claim 'groups' was returned, but 'oidc-group-field' is not set, check your coder oidc settings" )
1055
+ }
1056
+
1057
+ return usingGroups , groups , nil
1058
+ }
1059
+
1060
+ // oidcRoles returns the roles for the user from the OIDC claims.
1061
+ // If the function returns false, then the caller should return early.
1062
+ // All writes to the response writer are handled by this function.
1063
+ // It would be preferred to just return an error, however this function
1064
+ // decorates returned errors with the appropriate HTTP status codes and details
1065
+ // that are hard to carry in a standard `error` without more work.
1066
+ func (api * API ) oidcRoles (ctx context.Context , rw http.ResponseWriter , r * http.Request , mergedClaims map [string ]interface {}) ([]string , bool ) {
1067
+ roles := api .OIDCConfig .UserRolesDefault
1068
+ if ! api .OIDCConfig .RoleSyncEnabled () {
1069
+ return roles , true
1070
+ }
1071
+
1072
+ rolesRow , ok := mergedClaims [api .OIDCConfig .UserRoleField ]
1073
+ if ! ok {
1074
+ // If no claim is provided than we can assume the user is just
1075
+ // a member. This is because there is no way to tell the difference
1076
+ // between []string{} and nil for OIDC claims. IDPs omit claims
1077
+ // if they are empty ([]string{}).
1078
+ // Use []interface{}{} so the next typecast works.
1079
+ rolesRow = []interface {}{}
1080
+ }
1081
+
1082
+ rolesInterface , ok := rolesRow .([]interface {})
1083
+ if ! ok {
1084
+ api .Logger .Error (ctx , "oidc claim user roles field was an unknown type" ,
1085
+ slog .F ("type" , fmt .Sprintf ("%T" , rolesRow )),
1086
+ )
1087
+ site .RenderStaticErrorPage (rw , r , site.ErrorPageData {
1088
+ Status : http .StatusInternalServerError ,
1089
+ HideStatus : true ,
1090
+ Title : "Login disabled until OIDC config is fixed" ,
1091
+ Description : fmt .Sprintf ("Roles claim must be an array of strings, type found: %T. Disabling role sync will allow login to proceed." , rolesRow ),
1092
+ RetryEnabled : false ,
1093
+ DashboardURL : "/login" ,
1094
+ })
1095
+ return nil , false
1096
+ }
1097
+
1098
+ api .Logger .Debug (ctx , "roles returned in oidc claims" ,
1099
+ slog .F ("len" , len (rolesInterface )),
1100
+ slog .F ("roles" , rolesInterface ),
1101
+ )
1102
+ for _ , roleInterface := range rolesInterface {
1103
+ role , ok := roleInterface .(string )
1104
+ if ! ok {
1105
+ api .Logger .Error (ctx , "invalid oidc user role type" ,
1106
+ slog .F ("type" , fmt .Sprintf ("%T" , rolesRow )),
1107
+ )
1108
+ httpapi .Write (ctx , rw , http .StatusBadRequest , codersdk.Response {
1109
+ Message : fmt .Sprintf ("Invalid user role type. Expected string, got: %T" , roleInterface ),
1110
+ })
1111
+ return nil , false
1112
+ }
1113
+
1114
+ if mappedRoles , ok := api .OIDCConfig .UserRoleMapping [role ]; ok {
1115
+ if len (mappedRoles ) == 0 {
1116
+ continue
1117
+ }
1118
+ // Mapped roles are added to the list of roles
1119
+ roles = append (roles , mappedRoles ... )
1120
+ continue
1121
+ }
1122
+
1123
+ roles = append (roles , role )
1124
+ }
1125
+ return roles , true
1126
+ }
1127
+
1098
1128
// claimFields returns the sorted list of fields in the claims map.
1099
1129
func claimFields (claims map [string ]interface {}) []string {
1100
1130
fields := []string {}
0 commit comments