feat: add endpoint and CLI for users to view their own OIDC claims (#23053)

- Adds a new API endpoint `GET /api/v2/users/oidc-claims` that returns
only the **merged claims** (not the separate id_token/userinfo
breakdown). Scoped exclusively to the authenticated user's own identity
— no user parameter, so users cannot view each other's claims.
- Adds a new CLI command:** `coder users oidc-claims` that hits the
above endpoint.
- The existing owner-only debug endpoint is preserved unchanged for
admins who need the full claim breakdown.


> 🤖 This PR was created with the help of Coder Agents, and will be
reviewed by my human. 🧑‍💻
This commit is contained in:
Cian Johnston
2026-03-18 22:10:04 +00:00
committed by GitHub
parent a6856320f9
commit be1c06dec9
15 changed files with 524 additions and 19 deletions
+35
View File
@@ -7870,6 +7870,31 @@ const docTemplate = `{
]
}
},
"/users/oidc-claims": {
"get": {
"produces": [
"application/json"
],
"tags": [
"Users"
],
"summary": "Get OIDC claims for the authenticated user",
"operationId": "get-oidc-claims-for-the-authenticated-user",
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/codersdk.OIDCClaimsResponse"
}
}
},
"security": [
{
"CoderSessionToken": []
}
]
}
},
"/users/oidc/callback": {
"get": {
"tags": [
@@ -16886,6 +16911,16 @@ const docTemplate = `{
}
}
},
"codersdk.OIDCClaimsResponse": {
"type": "object",
"properties": {
"claims": {
"description": "Claims are the merged claims from the OIDC provider. These\nare the union of the ID token claims and the userinfo claims,\nwhere userinfo claims take precedence on conflict.",
"type": "object",
"additionalProperties": true
}
}
},
"codersdk.OIDCConfig": {
"type": "object",
"properties": {
+31
View File
@@ -6965,6 +6965,27 @@
]
}
},
"/users/oidc-claims": {
"get": {
"produces": ["application/json"],
"tags": ["Users"],
"summary": "Get OIDC claims for the authenticated user",
"operationId": "get-oidc-claims-for-the-authenticated-user",
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/codersdk.OIDCClaimsResponse"
}
}
},
"security": [
{
"CoderSessionToken": []
}
]
}
},
"/users/oidc/callback": {
"get": {
"tags": ["Users"],
@@ -15337,6 +15358,16 @@
}
}
},
"codersdk.OIDCClaimsResponse": {
"type": "object",
"properties": {
"claims": {
"description": "Claims are the merged claims from the OIDC provider. These\nare the union of the ID token claims and the userinfo claims,\nwhere userinfo claims take precedence on conflict.",
"type": "object",
"additionalProperties": true
}
}
},
"codersdk.OIDCConfig": {
"type": "object",
"properties": {
+1
View File
@@ -1496,6 +1496,7 @@ func New(options *Options) *API {
r.Post("/", api.postUser)
r.Get("/", api.users)
r.Post("/logout", api.postLogout)
r.Get("/oidc-claims", api.userOIDCClaims)
// These routes query information about site wide roles.
r.Route("/roles", func(r chi.Router) {
r.Get("/", api.AssignableSiteRoles)
+58
View File
@@ -72,6 +72,64 @@ func (api *API) userDebugOIDC(rw http.ResponseWriter, r *http.Request) {
httpapi.Write(ctx, rw, http.StatusOK, link.Claims)
}
// Returns the merged OIDC claims for the authenticated user.
//
// @Summary Get OIDC claims for the authenticated user
// @ID get-oidc-claims-for-the-authenticated-user
// @Security CoderSessionToken
// @Produce json
// @Tags Users
// @Success 200 {object} codersdk.OIDCClaimsResponse
// @Router /users/oidc-claims [get]
func (api *API) userOIDCClaims(rw http.ResponseWriter, r *http.Request) {
var (
ctx = r.Context()
apiKey = httpmw.APIKey(r)
)
user, err := api.Database.GetUserByID(ctx, apiKey.UserID)
if err != nil {
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
Message: "Failed to get user.",
Detail: err.Error(),
})
return
}
if user.LoginType != database.LoginTypeOIDC {
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
Message: "User is not an OIDC user.",
})
return
}
//nolint:gocritic // GetUserLinkByUserIDLoginType requires reading
// rbac.ResourceSystem. The endpoint is scoped to the authenticated
// user's own identity via apiKey, so this is safe.
link, err := api.Database.GetUserLinkByUserIDLoginType(
dbauthz.AsSystemRestricted(ctx),
database.GetUserLinkByUserIDLoginTypeParams{
UserID: user.ID,
LoginType: database.LoginTypeOIDC,
},
)
if err != nil {
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
Message: "Failed to get user link.",
Detail: err.Error(),
})
return
}
claims := link.Claims.MergedClaims
if claims == nil {
claims = map[string]interface{}{}
}
httpapi.Write(ctx, rw, http.StatusOK, codersdk.OIDCClaimsResponse{
Claims: claims,
})
}
// Returns whether the initial user has been created or not.
//
// @Summary Check initial user created