mirror of
https://github.com/pocket-id/pocket-id.git
synced 2026-07-17 00:09:53 +00:00
90 lines
3.2 KiB
Go
90 lines
3.2 KiB
Go
package oidc
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"net/http"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
"github.com/ory/fosite"
|
|
"gorm.io/gorm"
|
|
)
|
|
|
|
type userInfoHandler struct {
|
|
provider fosite.OAuth2Provider
|
|
claimsService *ClaimsService
|
|
issuer string
|
|
}
|
|
|
|
func newUserInfoHandler(provider fosite.OAuth2Provider, claimsService *ClaimsService, issuer string) *userInfoHandler {
|
|
return &userInfoHandler{
|
|
provider: provider,
|
|
claimsService: claimsService,
|
|
issuer: issuer,
|
|
}
|
|
}
|
|
|
|
// userInfo godoc
|
|
// @Summary Get user information
|
|
// @Description Get user information based on the access token
|
|
// @Tags OIDC
|
|
// @Accept json
|
|
// @Produce json
|
|
// @Success 200 {object} object "User claims based on requested scopes"
|
|
// @Security OAuth2AccessToken
|
|
// @Router /api/oidc/userinfo [get]
|
|
func (h *userInfoHandler) userInfo(c *gin.Context) {
|
|
ctx := c.Request.Context()
|
|
tokenType, accessRequest, err := h.provider.IntrospectToken(ctx, fosite.AccessTokenFromRequest(c.Request), fosite.AccessToken, NewEmptySession())
|
|
if err != nil {
|
|
writeUserInfoError(c, err)
|
|
return
|
|
}
|
|
if tokenType != fosite.AccessToken {
|
|
writeUserInfoError(c, fosite.ErrRequestUnauthorized.WithDescription("Only access tokens are allowed in the authorization header."))
|
|
return
|
|
}
|
|
|
|
session, ok := accessRequest.GetSession().(*Session)
|
|
if !ok || session.GetSubject() == "" {
|
|
writeUserInfoError(c, fosite.ErrRequestUnauthorized.WithDescription("The access token is invalid"))
|
|
return
|
|
}
|
|
|
|
// userinfo is one of Pocket ID's own identity endpoints, so the presented token must be audienced to Pocket ID itself (the issuer)
|
|
// A token granted an identity scope carries the issuer audience and is accepted here even when it also targets a custom API, while a token audienced only to a custom API belongs to that third-party resource server and cannot be replayed here to read the user's profile
|
|
if !accessRequest.GetGrantedAudience().Has(h.issuer) {
|
|
writeUserInfoError(c, fosite.ErrAccessDenied.WithDescription("The access token is not audienced to this server and cannot be used to access user information."))
|
|
return
|
|
}
|
|
|
|
// userinfo serves OIDC identity tokens, which are exactly the ones granted the openid scope
|
|
// An access token issued purely for a custom API never carries openid and is rejected here regardless of its audience
|
|
if !accessRequest.GetGrantedScopes().Has("openid") {
|
|
writeUserInfoError(c, fosite.ErrAccessDenied.WithDescription("The access token is missing the openid scope."))
|
|
return
|
|
}
|
|
|
|
claims, err := h.claimsService.GetUserClaims(ctx, session.GetSubject(), accessRequest.GetGrantedScopes())
|
|
if err != nil {
|
|
// A token whose subject no longer resolves to a user is an authentication failure, not a missing resource
|
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
writeUserInfoError(c, fosite.ErrRequestUnauthorized.WithDescription("The access token is invalid"))
|
|
return
|
|
}
|
|
_ = c.Error(err)
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, claims)
|
|
}
|
|
|
|
func writeUserInfoError(c *gin.Context, err error) {
|
|
rfcErr := fosite.ErrorToRFC6749Error(err)
|
|
if rfcErr.StatusCode() == http.StatusUnauthorized {
|
|
c.Header("WWW-Authenticate", fmt.Sprintf(`Bearer error="%s", error_description="%s"`, rfcErr.ErrorField, rfcErr.GetDescription()))
|
|
}
|
|
|
|
c.JSON(rfcErr.StatusCode(), rfcErr)
|
|
}
|