mirror of
https://github.com/pocket-id/pocket-id.git
synced 2026-10-24 18:49:04 +02:00
refactor: use fosite for OAuth 2.0 logic (#1520)
This commit is contained in:
@@ -1,11 +1,7 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -17,33 +13,17 @@ import (
|
||||
"github.com/pocket-id/pocket-id/backend/internal/middleware"
|
||||
"github.com/pocket-id/pocket-id/backend/internal/service"
|
||||
"github.com/pocket-id/pocket-id/backend/internal/utils"
|
||||
"github.com/pocket-id/pocket-id/backend/internal/utils/cookie"
|
||||
)
|
||||
|
||||
// NewOidcController creates a new controller for OIDC related endpoints
|
||||
// @Summary OIDC controller
|
||||
// @Description Initializes all OIDC-related API endpoints for authentication and client management
|
||||
// @Tags OIDC
|
||||
func NewOidcController(group *gin.RouterGroup, authMiddleware *middleware.AuthMiddleware, fileSizeLimitMiddleware *middleware.FileSizeLimitMiddleware, oidcService *service.OidcService, jwtService *service.JwtService) {
|
||||
func NewOidcController(group *gin.RouterGroup, authMiddleware *middleware.AuthMiddleware, fileSizeLimitMiddleware *middleware.FileSizeLimitMiddleware, oidcService *service.OidcService) {
|
||||
oc := &OidcController{
|
||||
oidcService: oidcService,
|
||||
jwtService: jwtService,
|
||||
createTokens: oidcService.CreateTokens,
|
||||
oidcService: oidcService,
|
||||
}
|
||||
|
||||
group.POST("/oidc/authorize", authMiddleware.WithAdminNotRequired().Add(), oc.authorizeHandler)
|
||||
group.POST("/oidc/authorize/callback-url", oc.authorizeCallbackURLHandler)
|
||||
group.POST("/oidc/authorization-required", authMiddleware.WithAdminNotRequired().Add(), oc.authorizationConfirmationRequiredHandler)
|
||||
group.GET("/oidc/par-request-info", authMiddleware.WithAdminNotRequired().Add(), oc.parRequestInfoHandler)
|
||||
|
||||
group.POST("/oidc/token", oc.createTokensHandler)
|
||||
group.POST("/oidc/par", oc.pushedAuthorizationRequestHandler)
|
||||
group.GET("/oidc/userinfo", oc.userInfoHandler)
|
||||
group.POST("/oidc/userinfo", oc.userInfoHandler)
|
||||
group.POST("/oidc/end-session", authMiddleware.WithAdminNotRequired().WithSuccessOptional().Add(), oc.EndSessionHandler)
|
||||
group.GET("/oidc/end-session", authMiddleware.WithAdminNotRequired().WithSuccessOptional().Add(), oc.EndSessionHandler)
|
||||
group.POST("/oidc/introspect", oc.introspectTokenHandler)
|
||||
|
||||
group.GET("/oidc/clients", authMiddleware.Add(), oc.listClientsHandler)
|
||||
group.POST("/oidc/clients", authMiddleware.Add(), oc.createClientHandler)
|
||||
group.GET("/oidc/clients/:id", authMiddleware.Add(), oc.getClientHandler)
|
||||
@@ -60,10 +40,6 @@ func NewOidcController(group *gin.RouterGroup, authMiddleware *middleware.AuthMi
|
||||
|
||||
group.GET("/oidc/clients/:id/preview/:userId", authMiddleware.Add(), oc.getClientPreviewHandler)
|
||||
|
||||
group.POST("/oidc/device/authorize", oc.deviceAuthorizationHandler)
|
||||
group.POST("/oidc/device/verify", authMiddleware.WithAdminNotRequired().Add(), oc.verifyDeviceCodeHandler)
|
||||
group.GET("/oidc/device/info", authMiddleware.WithAdminNotRequired().Add(), oc.getDeviceCodeInfoHandler)
|
||||
|
||||
group.GET("/oidc/users/me/authorized-clients", authMiddleware.WithAdminNotRequired().Add(), oc.listOwnAuthorizedClientsHandler)
|
||||
group.GET("/oidc/users/:id/authorized-clients", authMiddleware.Add(), oc.listAuthorizedClientsHandler)
|
||||
|
||||
@@ -76,429 +52,7 @@ func NewOidcController(group *gin.RouterGroup, authMiddleware *middleware.AuthMi
|
||||
}
|
||||
|
||||
type OidcController struct {
|
||||
oidcService *service.OidcService
|
||||
jwtService *service.JwtService
|
||||
createTokens func(context.Context, dto.OidcCreateTokensDto) (service.CreatedTokens, error)
|
||||
}
|
||||
|
||||
// authorizeHandler godoc
|
||||
// @Summary Authorize OIDC client
|
||||
// @Description Start the OIDC authorization process for a client
|
||||
// @Tags OIDC
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param request body dto.AuthorizeOidcClientRequestDto true "Authorization request parameters"
|
||||
// @Success 200 {object} dto.AuthorizeOidcClientResponseDto "Authorization code and callback URL"
|
||||
// @Router /api/oidc/authorize [post]
|
||||
func (oc *OidcController) authorizeHandler(c *gin.Context) {
|
||||
var input dto.AuthorizeOidcClientRequestDto
|
||||
err := c.ShouldBindJSON(&input)
|
||||
if err != nil {
|
||||
_ = c.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
code, callbackURL, err := oc.oidcService.Authorize(
|
||||
c.Request.Context(),
|
||||
input,
|
||||
c.GetString("userID"),
|
||||
c.GetString("authenticationMethod"),
|
||||
c.ClientIP(),
|
||||
c.Request.UserAgent(),
|
||||
)
|
||||
if err != nil {
|
||||
// Check if this is a prompt-related error that should be returned as a redirect error
|
||||
if isOidcPromptError(err) {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"error": err.Error(),
|
||||
"requiresRedirect": true,
|
||||
"callbackURL": callbackURL,
|
||||
})
|
||||
return
|
||||
}
|
||||
_ = c.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
response := dto.AuthorizeOidcClientResponseDto{
|
||||
Code: code,
|
||||
CallbackURL: callbackURL,
|
||||
Issuer: common.EnvConfig.AppURL,
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, response)
|
||||
}
|
||||
|
||||
// authorizeCallbackURLHandler godoc
|
||||
// @Summary Resolve a validated callback URL for an OIDC authorization request
|
||||
// @Description Resolves the redirect URI against the client's configured callback URLs without consuming PAR records.
|
||||
// @Tags OIDC
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param request body dto.AuthorizeOidcClientCallbackRequestDto true "Authorization callback parameters"
|
||||
// @Success 200 {object} dto.AuthorizeOidcClientCallbackResponseDto "Resolved callback URL"
|
||||
// @Router /api/oidc/authorize/callback-url [post]
|
||||
func (oc *OidcController) authorizeCallbackURLHandler(c *gin.Context) {
|
||||
var input dto.AuthorizeOidcClientCallbackRequestDto
|
||||
if err := c.ShouldBindJSON(&input); err != nil {
|
||||
_ = c.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
callbackURL, err := oc.oidcService.ResolveAuthorizeCallbackURL(
|
||||
c.Request.Context(),
|
||||
input.ClientID,
|
||||
input.CallbackURL,
|
||||
input.RequestURI,
|
||||
)
|
||||
if err != nil {
|
||||
_ = c.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, dto.AuthorizeOidcClientCallbackResponseDto{CallbackURL: callbackURL})
|
||||
}
|
||||
|
||||
// isOidcPromptError checks if an error is a prompt-related OIDC error that should trigger a redirect
|
||||
func isOidcPromptError(err error) bool {
|
||||
var loginReq *common.OidcLoginRequiredError
|
||||
var consentReq *common.OidcConsentRequiredError
|
||||
var interactionReq *common.OidcInteractionRequiredError
|
||||
var accountSelectionReq *common.OidcAccountSelectionRequiredError
|
||||
|
||||
return errors.As(err, &loginReq) ||
|
||||
errors.As(err, &consentReq) ||
|
||||
errors.As(err, &interactionReq) ||
|
||||
errors.As(err, &accountSelectionReq)
|
||||
}
|
||||
|
||||
// authorizationConfirmationRequiredHandler godoc
|
||||
// @Summary Check if authorization confirmation is required
|
||||
// @Description Check if the user needs to confirm authorization for the client
|
||||
// @Tags OIDC
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param request body dto.AuthorizationRequiredDto true "Authorization check parameters"
|
||||
// @Success 200 {object} object "{ \"authorizationRequired\": true/false }"
|
||||
// @Router /api/oidc/authorization-required [post]
|
||||
func (oc *OidcController) authorizationConfirmationRequiredHandler(c *gin.Context) {
|
||||
var input dto.AuthorizationRequiredDto
|
||||
if err := c.ShouldBindJSON(&input); err != nil {
|
||||
_ = c.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
authorizationRequired, scope, err := oc.oidcService.AuthorizationRequired(c.Request.Context(), input.ClientID, c.GetString("userID"), input.Scope, input.RequestURI)
|
||||
if err != nil {
|
||||
_ = c.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"authorizationRequired": authorizationRequired, "scope": scope})
|
||||
}
|
||||
|
||||
// parRequestInfoHandler godoc
|
||||
// @Summary Resolve stored authorization request parameters
|
||||
// @Description Resolve the parameters of a stored request_uri request so the consent page can render the requested scope and build the final redirect. Does not consume the request.
|
||||
// @Tags OIDC
|
||||
// @Produce json
|
||||
// @Param client_id query string true "Client ID"
|
||||
// @Param request_uri query string true "Request URI returned from the PAR endpoint"
|
||||
// @Success 200 {object} dto.OidcAuthorizeRequestInfoDto "Resolved authorization request parameters"
|
||||
// @Router /api/oidc/par-request-info [get]
|
||||
func (oc *OidcController) parRequestInfoHandler(c *gin.Context) {
|
||||
clientID := c.Query("client_id")
|
||||
requestURI := c.Query("request_uri")
|
||||
if clientID == "" {
|
||||
_ = c.Error(&common.ValidationError{Message: "client_id is required"})
|
||||
return
|
||||
}
|
||||
if requestURI == "" {
|
||||
_ = c.Error(&common.ValidationError{Message: "request_uri is required"})
|
||||
return
|
||||
}
|
||||
|
||||
info, err := oc.oidcService.GetPushedAuthorizationRequest(c.Request.Context(), clientID, requestURI)
|
||||
if err != nil {
|
||||
_ = c.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
var infoDto dto.OidcAuthorizeRequestInfoDto
|
||||
err = dto.MapStruct(info.Parameters, &infoDto)
|
||||
if err != nil {
|
||||
_ = c.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, infoDto)
|
||||
}
|
||||
|
||||
// createTokensHandler godoc
|
||||
// @Summary Create OIDC tokens
|
||||
// @Description Exchange authorization code or refresh token for access tokens
|
||||
// @Tags OIDC
|
||||
// @Produce json
|
||||
// @Param client_id formData string false "Client ID (if not using Basic Auth)"
|
||||
// @Param client_secret formData string false "Client secret (if not using Basic Auth or client assertions)"
|
||||
// @Param code formData string false "Authorization code (required for 'authorization_code' grant)"
|
||||
// @Param grant_type formData string true "Grant type ('authorization_code' or 'refresh_token')"
|
||||
// @Param code_verifier formData string false "PKCE code verifier (for authorization_code with PKCE)"
|
||||
// @Param refresh_token formData string false "Refresh token (required for 'refresh_token' grant)"
|
||||
// @Param client_assertion formData string false "Client assertion type (for 'authorization_code' grant when using client assertions)"
|
||||
// @Param client_assertion_type formData string false "Client assertion type (for 'authorization_code' grant when using client assertions)"
|
||||
// @Success 200 {object} dto.OidcTokenResponseDto "Token response with access_token and optional id_token and refresh_token"
|
||||
// @Router /api/oidc/token [post]
|
||||
func (oc *OidcController) createTokensHandler(c *gin.Context) {
|
||||
// Per RFC-6749, parameters passed to the /token endpoint MUST be passed in the body of the request
|
||||
// Gin's "ShouldBind" by default reads from the query string too, so we need to reset all query string args before invoking ShouldBind
|
||||
c.Request.URL.RawQuery = ""
|
||||
|
||||
var input dto.OidcCreateTokensDto
|
||||
err := c.ShouldBind(&input)
|
||||
if err != nil {
|
||||
_ = c.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
// Validate that code is provided for authorization_code grant type
|
||||
if input.GrantType == service.GrantTypeAuthorizationCode && input.Code == "" {
|
||||
_ = c.Error(&common.OidcMissingAuthorizationCodeError{})
|
||||
return
|
||||
}
|
||||
|
||||
// Validate that refresh_token is provided for refresh_token grant type
|
||||
if input.GrantType == service.GrantTypeRefreshToken && input.RefreshToken == "" {
|
||||
_ = c.Error(&common.OidcMissingRefreshTokenError{})
|
||||
return
|
||||
}
|
||||
|
||||
// Check if the client ID / secret are passed in the Authorization header (RFC 6749)
|
||||
parseBasicAuth(c.Request, &input.ClientSecret, &input.ClientID)
|
||||
|
||||
tokens, err := oc.createTokens(c.Request.Context(), input)
|
||||
|
||||
switch {
|
||||
case errors.Is(err, &common.OidcAuthorizationPendingError{}):
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"error": "authorization_pending",
|
||||
})
|
||||
return
|
||||
case errors.Is(err, &common.OidcSlowDownError{}):
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"error": "slow_down",
|
||||
})
|
||||
return
|
||||
case err != nil:
|
||||
_ = c.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, dto.OidcTokenResponseDto{
|
||||
AccessToken: tokens.AccessToken,
|
||||
TokenType: "Bearer",
|
||||
ExpiresIn: int(tokens.ExpiresIn.Seconds()),
|
||||
IdToken: tokens.IdToken, // May be empty
|
||||
RefreshToken: tokens.RefreshToken, // May be empty
|
||||
})
|
||||
}
|
||||
|
||||
// pushedAuthorizationRequestHandler godoc
|
||||
// @Summary Pushed Authorization Request (PAR)
|
||||
// @Description RFC 9126: Push authorization request parameters and receive a request_uri. Only confidential clients may use this endpoint.
|
||||
// @Tags OIDC
|
||||
// @Accept application/x-www-form-urlencoded
|
||||
// @Produce json
|
||||
// @Success 201 {object} dto.OidcPARResponseDto
|
||||
// @Router /api/oidc/par [post]
|
||||
func (oc *OidcController) pushedAuthorizationRequestHandler(c *gin.Context) {
|
||||
// Per RFC 9126, parameters MUST be passed in the request body
|
||||
c.Request.URL.RawQuery = ""
|
||||
|
||||
var input dto.OidcPARRequestDto
|
||||
if err := c.ShouldBind(&input); err != nil {
|
||||
_ = c.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
// Client id and secret can also be passed over the Authorization header
|
||||
if input.ClientID == "" && input.ClientSecret == "" {
|
||||
input.ClientID, input.ClientSecret, _ = utils.OAuthClientBasicAuth(c.Request)
|
||||
}
|
||||
|
||||
creds := service.ClientAuthCredentials{
|
||||
ClientID: input.ClientID,
|
||||
ClientSecret: input.ClientSecret,
|
||||
ClientAssertion: input.ClientAssertion,
|
||||
ClientAssertionType: input.ClientAssertionType,
|
||||
}
|
||||
|
||||
requestURI, expiresIn, err := oc.oidcService.CreatePushedAuthorizationRequest(c.Request.Context(), creds, input)
|
||||
if err != nil {
|
||||
_ = c.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
// RFC 9126 §2.2 requires HTTP 201 Created for successful PAR responses
|
||||
c.JSON(http.StatusCreated, dto.OidcPARResponseDto{
|
||||
RequestURI: requestURI,
|
||||
ExpiresIn: expiresIn,
|
||||
})
|
||||
}
|
||||
|
||||
// userInfoHandler 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 (oc *OidcController) userInfoHandler(c *gin.Context) {
|
||||
_, authToken, ok := strings.Cut(c.GetHeader("Authorization"), " ")
|
||||
if !ok || authToken == "" {
|
||||
_ = c.Error(&common.MissingAccessToken{})
|
||||
return
|
||||
}
|
||||
|
||||
token, err := oc.jwtService.VerifyOAuthAccessToken(authToken)
|
||||
if err != nil {
|
||||
_ = c.Error(err)
|
||||
return
|
||||
}
|
||||
userID, ok := token.Subject()
|
||||
if !ok {
|
||||
_ = c.Error(&common.TokenInvalidError{})
|
||||
return
|
||||
}
|
||||
clientID, ok := token.Audience()
|
||||
if !ok || len(clientID) != 1 {
|
||||
_ = c.Error(&common.TokenInvalidError{})
|
||||
return
|
||||
}
|
||||
claims, err := oc.oidcService.GetUserClaimsForClient(c.Request.Context(), userID, clientID[0])
|
||||
if err != nil {
|
||||
_ = c.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, claims)
|
||||
}
|
||||
|
||||
// EndSessionHandler godoc
|
||||
// @Summary End OIDC session
|
||||
// @Description End user session and handle OIDC logout
|
||||
// @Tags OIDC
|
||||
// @Accept application/x-www-form-urlencoded
|
||||
// @Param id_token_hint query string false "ID token"
|
||||
// @Param post_logout_redirect_uri query string false "URL to redirect to after logout"
|
||||
// @Param state query string false "State parameter to include in the redirect"
|
||||
// @Success 302 "Redirect to post-logout URL or application logout page"
|
||||
// @Router /api/oidc/end-session [get]
|
||||
func (oc *OidcController) EndSessionHandler(c *gin.Context) {
|
||||
var input dto.OidcLogoutDto
|
||||
|
||||
// Bind query parameters to the struct
|
||||
switch c.Request.Method {
|
||||
case http.MethodGet:
|
||||
if err := c.ShouldBindQuery(&input); err != nil {
|
||||
_ = c.Error(err)
|
||||
return
|
||||
}
|
||||
case http.MethodPost:
|
||||
// Bind form parameters to the struct
|
||||
if err := c.ShouldBind(&input); err != nil {
|
||||
_ = c.Error(err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
callbackURL, err := oc.oidcService.ValidateEndSession(c.Request.Context(), input, c.GetString("userID"))
|
||||
if err != nil {
|
||||
// If the validation fails, the user has to confirm the logout manually and doesn't get redirected
|
||||
slog.WarnContext(c.Request.Context(), "Error getting logout callback URL, the user has to confirm the logout manually", "error", err)
|
||||
c.Redirect(http.StatusFound, common.EnvConfig.AppURL+"/logout")
|
||||
return
|
||||
}
|
||||
|
||||
// The validation was successful, so we can log out and redirect the user to the callback URL without confirmation
|
||||
cookie.AddAccessTokenCookie(c, 0, "")
|
||||
|
||||
// Callback URL can be empty if none is configured
|
||||
if callbackURL == "" {
|
||||
c.Redirect(http.StatusFound, common.EnvConfig.AppURL+"/logout")
|
||||
return
|
||||
}
|
||||
|
||||
logoutCallbackURL, _ := url.Parse(callbackURL)
|
||||
if input.State != "" {
|
||||
q := logoutCallbackURL.Query()
|
||||
q.Set("state", input.State)
|
||||
logoutCallbackURL.RawQuery = q.Encode()
|
||||
}
|
||||
|
||||
c.Redirect(http.StatusFound, logoutCallbackURL.String())
|
||||
}
|
||||
|
||||
// EndSessionHandler godoc (POST method)
|
||||
// @Summary End OIDC session (POST method)
|
||||
// @Description End user session and handle OIDC logout using POST
|
||||
// @Tags OIDC
|
||||
// @Accept application/x-www-form-urlencoded
|
||||
// @Produce html
|
||||
// @Param id_token_hint formData string false "ID token"
|
||||
// @Param post_logout_redirect_uri formData string false "URL to redirect to after logout"
|
||||
// @Param state formData string false "State parameter to include in the redirect"
|
||||
// @Success 302 "Redirect to post-logout URL or application logout page"
|
||||
// @Router /api/oidc/end-session [post]
|
||||
func (oc *OidcController) EndSessionHandlerPost(c *gin.Context) {
|
||||
// Implementation is the same as GET
|
||||
}
|
||||
|
||||
// introspectToken godoc
|
||||
// @Summary Introspect OIDC tokens
|
||||
// @Description Pass an access_token to verify if it is considered valid.
|
||||
// @Tags OIDC
|
||||
// @Produce json
|
||||
// @Param token formData string true "The token to be introspected."
|
||||
// @Success 200 {object} dto.OidcIntrospectionResponseDto "Response with the introspection result."
|
||||
// @Router /api/oidc/introspect [post]
|
||||
func (oc *OidcController) introspectTokenHandler(c *gin.Context) {
|
||||
var input dto.OidcIntrospectDto
|
||||
if err := c.ShouldBind(&input); err != nil {
|
||||
_ = c.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
// Client id and secret have to be passed over the Authorization header. This kind of
|
||||
// authentication allows us to keep the endpoint protected (since it could be used to
|
||||
// find valid tokens) while still allowing it to be used by an application that is
|
||||
// supposed to interact with our IdP (since that needs to have a client_id
|
||||
// and client_secret anyway).
|
||||
var (
|
||||
creds service.ClientAuthCredentials
|
||||
ok bool
|
||||
)
|
||||
creds.ClientID, creds.ClientSecret, ok = utils.OAuthClientBasicAuth(c.Request)
|
||||
if !ok {
|
||||
// If there's no basic auth, check if we have a bearer token (used as client assertion)
|
||||
bearer, ok := utils.BearerAuth(c.Request)
|
||||
if ok {
|
||||
creds.ClientAssertionType = service.ClientAssertionTypeJWTBearer
|
||||
creds.ClientAssertion = bearer
|
||||
// When using client assertions, client_id can be passed as a form field
|
||||
creds.ClientID = input.ClientID
|
||||
}
|
||||
}
|
||||
|
||||
response, err := oc.oidcService.IntrospectToken(c.Request.Context(), creds, input.Token)
|
||||
if err != nil {
|
||||
_ = c.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, response)
|
||||
oidcService *service.OidcService
|
||||
}
|
||||
|
||||
// getClientMetaDataHandler godoc
|
||||
@@ -810,46 +364,6 @@ func (oc *OidcController) updateAllowedUserGroupsHandler(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, oidcClientDto)
|
||||
}
|
||||
|
||||
// This method can modify the value of clientSecret and clientID
|
||||
func parseBasicAuth(r *http.Request, clientSecret *string, clientID *string) {
|
||||
// Client id and secret can also be passed via the Authorization header (RFC 6749, section 2.3.1 "client_secret_basic")
|
||||
// When PKCE is used, some libraries send client_id in the body for the code_verifier binding while keeping the secret only in the Authorization header
|
||||
// We therefore fall back to Basic auth whenever the secret is missing, not only when both fields are empty
|
||||
if *clientSecret == "" {
|
||||
basicID, basicSecret, ok := utils.OAuthClientBasicAuth(r)
|
||||
if ok {
|
||||
if *clientID == "" {
|
||||
*clientID = basicID
|
||||
}
|
||||
*clientSecret = basicSecret
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (oc *OidcController) deviceAuthorizationHandler(c *gin.Context) {
|
||||
// Per RFC 8628 (OAuth 2.0 Device Authorization Grant), parameters for the device authorization request MUST be sent in the body of the POST request
|
||||
// Gin's "ShouldBind" by default reads from the query string too, so we need to reset all query string args before invoking ShouldBind
|
||||
c.Request.URL.RawQuery = ""
|
||||
|
||||
var input dto.OidcDeviceAuthorizationRequestDto
|
||||
err := c.ShouldBind(&input)
|
||||
if err != nil {
|
||||
_ = c.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
// Check if the client ID / secret are passed in the Authorization header (RFC 6749)
|
||||
parseBasicAuth(c.Request, &input.ClientSecret, &input.ClientID)
|
||||
|
||||
response, err := oc.oidcService.CreateDeviceAuthorization(c.Request.Context(), input)
|
||||
if err != nil {
|
||||
_ = c.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, response)
|
||||
}
|
||||
|
||||
// listOwnAuthorizedClientsHandler godoc
|
||||
// @Summary List authorized clients for current user
|
||||
// @Description Get a paginated list of OIDC clients that the current user has authorized
|
||||
@@ -951,49 +465,6 @@ func (oc *OidcController) listOwnAccessibleClientsHandler(c *gin.Context) {
|
||||
})
|
||||
}
|
||||
|
||||
func (oc *OidcController) verifyDeviceCodeHandler(c *gin.Context) {
|
||||
userCode := c.Query("code")
|
||||
if userCode == "" {
|
||||
_ = c.Error(&common.ValidationError{Message: "code is required"})
|
||||
return
|
||||
}
|
||||
|
||||
// Get IP address and user agent from the request context
|
||||
ipAddress := c.ClientIP()
|
||||
userAgent := c.Request.UserAgent()
|
||||
|
||||
err := oc.oidcService.VerifyDeviceCode(
|
||||
c.Request.Context(),
|
||||
userCode,
|
||||
c.GetString("userID"),
|
||||
c.GetString("authenticationMethod"),
|
||||
ipAddress,
|
||||
userAgent)
|
||||
|
||||
if err != nil {
|
||||
_ = c.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
c.Status(http.StatusNoContent)
|
||||
}
|
||||
|
||||
func (oc *OidcController) getDeviceCodeInfoHandler(c *gin.Context) {
|
||||
userCode := c.Query("code")
|
||||
if userCode == "" {
|
||||
_ = c.Error(&common.ValidationError{Message: "code is required"})
|
||||
return
|
||||
}
|
||||
|
||||
deviceCodeInfo, err := oc.oidcService.GetDeviceCodeInfo(c.Request.Context(), userCode, c.GetString("userID"))
|
||||
if err != nil {
|
||||
_ = c.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, deviceCodeInfo)
|
||||
}
|
||||
|
||||
// getClientPreviewHandler godoc
|
||||
// @Summary Preview OIDC client data for user
|
||||
// @Description Get a preview of the OIDC data (ID token, access token, userinfo) that would be sent to the client for a specific user
|
||||
|
||||
Reference in New Issue
Block a user