Files
Курнат Андрей 2315f25754 Initial QSfera import
2026-06-07 10:20:04 +03:00

948 lines
30 KiB
Go

/*
* Copyright 2017-2021 Kopano and its licensors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package provider
import (
"context"
"fmt"
"net/http"
"strings"
"github.com/go-jose/go-jose/v3"
"github.com/golang-jwt/jwt/v5"
"github.com/libregraph/oidc-go"
"github.com/longsleep/rndm"
"github.com/sirupsen/logrus"
konnect "github.com/libregraph/lico"
"github.com/libregraph/lico/identity"
"github.com/libregraph/lico/identity/clients"
konnectoidc "github.com/libregraph/lico/oidc"
"github.com/libregraph/lico/oidc/code"
"github.com/libregraph/lico/oidc/payload"
"github.com/libregraph/lico/utils"
)
const (
registrationSizeLimit = 1024 * 512
)
// WellKnownHandler implements the HTTP provider configuration endpoint
// for OpenID Connect 1.0 as specified at https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderConfig
func (p *Provider) WellKnownHandler(rw http.ResponseWriter, req *http.Request) {
// TODO(longsleep): Add caching headers.
wellKnown := p.metadata
err := utils.WriteJSON(rw, http.StatusOK, wellKnown, "")
if err != nil {
p.logger.WithError(err).Errorln("well-known request failed writing response")
}
}
// JwksHandler implements the HTTP provider JWKS endpoint for OpenID provider
// metadata used with OpenID Connect Discovery 1.0 as specified at https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderMetadata
func (p *Provider) JwksHandler(rw http.ResponseWriter, req *http.Request) {
addResponseHeaders(rw.Header())
validationKeys := p.validationKeys
jwks := &jose.JSONWebKeySet{
Keys: make([]jose.JSONWebKey, 0),
}
for kid, key := range validationKeys {
certificates, _ := p.certificates[kid]
keyJwk := jose.JSONWebKey{
Key: key,
KeyID: kid,
Use: "sig", // https://tools.ietf.org/html/rfc7517#section-4.2
Certificates: certificates,
}
if keyJwk.Valid() {
jwks.Keys = append(jwks.Keys, keyJwk.Public())
}
}
err := utils.WriteJSON(rw, http.StatusOK, jwks, "application/jwk-set+json")
if err != nil {
p.logger.WithError(err).Errorln("jwks request failed writing response")
}
}
// AuthorizeHandler implements the HTTP authorization endpoint for OpenID
// Connect 1.0 as specified at http://openid.net/specs/openid-connect-core-1_0.html#ImplicitAuthorizationEndpoint
//
// Currently AuthorizeHandler implements only the Implicit Flow as specified at
// http://openid.net/specs/openid-connect-core-1_0.html#ImplicitFlowAuth
func (p *Provider) AuthorizeHandler(rw http.ResponseWriter, req *http.Request) {
var err error
var auth identity.AuthRecord
addResponseHeaders(rw.Header())
ctx := konnect.NewRequestContext(req.Context(), req)
// OpenID Connect 1.0 authentication request validation.
// http://openid.net/specs/openid-connect-core-1_0.html#ImplicitValidation
err = req.ParseForm()
if err != nil {
p.logger.WithError(err).Errorln("authorize request invalid form data")
p.ErrorPage(rw, http.StatusBadRequest, oidc.ErrorCodeOAuth2InvalidRequest, err.Error())
return
}
ar, err := payload.DecodeAuthenticationRequest(req, p.metadata, func(token *jwt.Token) (interface{}, error) {
if claims, ok := token.Claims.(*payload.RequestObjectClaims); ok {
// Validate signed request tokens according to spec defined at
// https://openid.net/specs/openid-connect-core-1_0.html#SignedRequestObject
registration, _ := p.clients.Get(ctx, claims.ClientID)
if registration != nil {
if registration.RawRequestObjectSigningAlg != "" {
if token.Method.Alg() != registration.RawRequestObjectSigningAlg {
return nil, fmt.Errorf("token alg does not match client registration")
}
}
if token.Method == jwt.SigningMethodNone {
// Request parameters do not need to be signed to be valid, so
// none is allowed in this special case.
return jwt.UnsafeAllowNoneSignatureType, nil
}
// Get secure client.
if registration.JWKS != nil {
secureClient, err := registration.Secure(token.Header[oidc.JWTHeaderKeyID])
if err != nil {
return nil, err
}
if err := claims.SetSecure(secureClient); err != nil {
return nil, err
}
return secureClient.PublicKey, err
}
return nil, fmt.Errorf("no client keys registered")
} else {
// Also allow, when client is not registered and the token is unsigned.
if token.Method == jwt.SigningMethodNone {
// Request parameters do not need to be signed to be valid, so
// none is allowed in this special case.
return jwt.UnsafeAllowNoneSignatureType, nil
}
}
}
return nil, fmt.Errorf("not validated")
})
if err != nil {
p.logger.WithFields(utils.ErrorAsFields(err)).Errorln("authorize request invalid request data")
p.ErrorPage(rw, http.StatusBadRequest, oidc.ErrorCodeOAuth2InvalidRequest, err.Error())
return
}
err = ar.Validate(func(token *jwt.Token) (interface{}, error) {
// Validator for incoming IDToken hints, looks up key.
return p.validateJWT(token)
})
if err != nil {
goto done
}
// Inject implicit scopes set by client registration.
if registration, _ := p.clients.Get(ctx, ar.ClientID); registration != nil {
err = registration.ApplyImplicitScopes(ar.Scopes)
if err != nil {
p.logger.WithError(err).Debugln("failed to apply implicit scopes")
}
}
// Find session if any, ignoring errors.
ar.Session, err = p.getSession(req)
if err != nil {
p.logger.WithError(err).Debugln("failed to decode client session")
}
// Authorization Server Authenticates End-User
// http://openid.net/specs/openid-connect-core-1_0.html#ImplicitAuthenticates
auth, err = p.identityManager.Authenticate(ctx, rw, req, ar, p.guestManager)
if err != nil {
goto done
}
// Additional validation based on requested ID token claims.
if ar.Claims != nil && ar.Claims.IDToken != nil {
// Validate sub claim request
// https://openid.net/specs/openid-connect-core-1_0.html#ImplicitValidation
if subRequest, ok := ar.Claims.IDToken.Get(oidc.SubjectIdentifierClaim); ok {
if !subRequest.Match(auth.Subject()) {
err = ar.NewError(oidc.ErrorCodeOAuth2AccessDenied, "sub claim request mismatch")
goto done
}
}
}
// Authorization Server Obtains End-User Consent/Authorization
// http://openid.net/specs/openid-connect-core-1_0.html#ImplicitConsent
auth, err = auth.Manager().Authorize(ctx, rw, req, ar, auth)
if err != nil {
goto done
}
done:
p.AuthorizeResponse(rw, req, ar, auth, err)
}
// AuthorizeResponse writes the result according to the provided parameters to
// the provided http.ResponseWriter.
func (p *Provider) AuthorizeResponse(rw http.ResponseWriter, req *http.Request, ar *payload.AuthenticationRequest, auth identity.AuthRecord, err error) {
var codeString string
var accessTokenString string
var idTokenString string
var authorizedScopes map[string]bool
var session *payload.Session
var ctx context.Context
if err != nil {
goto done
}
ctx = identity.NewContext(konnect.NewRequestContext(req.Context(), req), auth)
// Create session.
session, err = p.updateOrCreateSession(rw, req, ar, auth)
if err != nil {
goto done
}
authorizedScopes = auth.AuthorizedScopes()
// Create code when requested.
if _, ok := ar.ResponseTypes[oidc.ResponseTypeCode]; ok {
codeString, err = p.codeManager.Create(&code.Record{
AuthenticationRequest: ar,
Auth: auth,
Session: session,
})
if err != nil {
goto done
}
}
// Create access token when requested.
if _, ok := ar.ResponseTypes[oidc.ResponseTypeToken]; ok {
accessTokenString, err = p.makeAccessToken(ctx, ar.ClientID, auth, nil, nil)
if err != nil {
goto done
}
}
// Create ID token when requested and granted.
if authorizedScopes[oidc.ScopeOpenID] {
if _, ok := ar.ResponseTypes[oidc.ResponseTypeIDToken]; ok {
idTokenString, err = p.makeIDToken(ctx, ar, auth, session, accessTokenString, codeString, nil, nil)
if err != nil {
goto done
}
}
}
done:
// Always set browser state.
browserState, browserStateErr := p.makeBrowserState(ar, auth, err)
if browserStateErr != nil {
p.logger.WithError(err).Errorln("failed to make browser state")
}
if browserStateErr = p.setBrowserStateCookie(rw, browserState); browserStateErr != nil {
p.logger.WithError(err).Errorln("failed to set browser state cookie")
}
if err != nil {
switch err.(type) {
case *payload.AuthenticationError:
p.Found(rw, ar.RedirectURI, err, ar.UseFragment)
case *payload.AuthenticationBadRequest:
p.ErrorPage(rw, http.StatusBadRequest, err.Error(), err.(*payload.AuthenticationBadRequest).Description())
case *identity.RedirectError:
p.Found(rw, err.(*identity.RedirectError).RedirectURI(), nil, false)
case *identity.LoginRequiredError:
p.LoginRequiredPage(rw, req, err.(*identity.LoginRequiredError).SignInURI())
case *identity.IsHandledError:
// do nothing
case *konnectoidc.OAuth2Error:
err = ar.NewError(err.Error(), err.(*konnectoidc.OAuth2Error).Description())
p.Found(rw, ar.RedirectURI, err, ar.UseFragment)
default:
p.logger.WithFields(utils.ErrorAsFields(err)).Errorln("authorize request failed")
p.ErrorPage(rw, http.StatusInternalServerError, err.Error(), "well sorry, but there was a problem")
}
return
}
sessionState, sessionStateErr := p.makeSessionState(req, ar, browserState)
if sessionStateErr != nil {
p.logger.WithError(err).Errorln("failed to make session state")
}
authorizedScopesList := makeArrayFromBoolMap(authorizedScopes)
// Successful Authentication Response
// http://openid.net/specs/openid-connect-core-1_0.html#ImplicitAuthResponse
response := &payload.AuthenticationSuccess{
State: ar.State,
Scope: strings.Join(authorizedScopesList, " "),
SessionState: sessionState,
}
if codeString != "" {
response.Code = codeString
}
if accessTokenString != "" {
response.AccessToken = accessTokenString
response.TokenType = oidc.TokenTypeBearer
response.ExpiresIn = int64(p.accessTokenDuration.Seconds())
}
if idTokenString != "" {
response.IDToken = idTokenString
}
p.Found(rw, ar.RedirectURI, response, ar.UseFragment)
}
// TokenHandler implements the HTTP token endpoint for OpenID
// Connect 1.0 as specified at http://openid.net/specs/openid-connect-core-1_0.html#TokenEndpoint
func (p *Provider) TokenHandler(rw http.ResponseWriter, req *http.Request) {
var err error
var tr *payload.TokenRequest
var found bool
var ar *payload.AuthenticationRequest
var auth identity.AuthRecord
var session *payload.Session
var accessTokenString string
var idTokenString string
var refreshTokenString string
var refreshTokenClaims *konnect.RefreshTokenClaims
var approvedScopes map[string]bool
var authorizedScopes map[string]bool
var clientDetails *clients.Details
signinMethod := p.signingMethodDefault
rw.Header().Set("Cache-Control", "no-store")
rw.Header().Set("Pragma", "no-cache")
ctx := konnect.NewRequestContext(req.Context(), req)
// Validate request method
switch req.Method {
case http.MethodPost:
// breaks
default:
err = konnectoidc.NewOAuth2Error(oidc.ErrorCodeOAuth2InvalidRequest, "request must be sent with POST")
goto done
}
// Token Request Validation
// http://openid.net/specs/openid-connect-core-1_0.html#TokenRequestValidation
err = req.ParseForm()
if err != nil {
err = konnectoidc.NewOAuth2Error(oidc.ErrorCodeOAuth2InvalidRequest, err.Error())
goto done
}
tr, err = payload.DecodeTokenRequest(req, p.metadata)
if err != nil {
err = konnectoidc.NewOAuth2Error(oidc.ErrorCodeOAuth2InvalidRequest, err.Error())
goto done
}
err = tr.Validate(func(token *jwt.Token) (interface{}, error) {
// Validator for incoming refresh tokens, looks up key.
return p.validateJWT(token)
}, &konnect.RefreshTokenClaims{})
if err != nil {
goto done
}
// Additional validations according to https://tools.ietf.org/html/rfc6749#section-4.1.3
clientDetails, err = p.clients.Lookup(ctx, tr.ClientID, tr.ClientSecret, tr.RedirectURI, "", false)
if err != nil {
err = konnectoidc.NewOAuth2Error(oidc.ErrorCodeOAuth2AccessDenied, err.Error())
goto done
}
if clientDetails != nil && clientDetails.Registration != nil {
signinMethod = jwt.GetSigningMethod(clientDetails.Registration.RawIDTokenSignedResponseAlg)
}
switch tr.GrantType {
case oidc.GrantTypeAuthorizationCode:
codeRecord, codeRecordFound := p.codeManager.Pop(tr.Code)
if !codeRecordFound {
err = konnectoidc.NewOAuth2Error(oidc.ErrorCodeOAuth2InvalidGrant, "code not found")
goto done
}
ar = codeRecord.AuthenticationRequest
auth = codeRecord.Auth
session = codeRecord.Session
authorizedScopes = auth.AuthorizedScopes()
// Ensure that the authorization code was issued to the client id.
if ar.ClientID != tr.ClientID {
err = konnectoidc.NewOAuth2Error(oidc.ErrorCodeOAuth2InvalidGrant, "client_id mismatch")
goto done
}
// Ensure that the "redirect_uri" parameter is a match.
if ar.RawRedirectURI != tr.RawRedirectURI {
err = konnectoidc.NewOAuth2Error(oidc.ErrorCodeOAuth2InvalidGrant, "redirect_uri mismatch")
goto done
}
// Validate code challenge according to https://tools.ietf.org/html/rfc7636#section-4.6
if tr.CodeVerifier != "" || ar.CodeChallenge != "" {
if codeVerifierErr := oidc.ValidateCodeChallenge(ar.CodeChallenge, ar.CodeChallengeMethod, tr.CodeVerifier); codeVerifierErr != nil {
err = konnectoidc.NewOAuth2Error(oidc.ErrorCodeOAuth2InvalidGrant, codeVerifierErr.Error())
goto done
}
}
if _, ok := identity.FromContext(ctx); !ok {
ctx = identity.NewContext(ctx, auth)
}
case oidc.GrantTypeRefreshToken:
if tr.RefreshToken == nil {
err = konnectoidc.NewOAuth2Error(oidc.ErrorCodeOAuth2InvalidGrant, "missing refresh_token")
goto done
}
// Get claims from refresh token.
claims := tr.RefreshToken.Claims.(*konnect.RefreshTokenClaims)
// Ensure that the authorization code was issued to the client id.
if claims.Audience[0] != tr.ClientID {
err = konnectoidc.NewOAuth2Error(oidc.ErrorCodeOAuth2InvalidGrant, "client_id mismatch")
goto done
}
// TODO(longsleep): Compare standard claims issuer.
userID, sessionRef := p.getUserIDAndSessionRefFromClaims(&claims.RegisteredClaims, nil, claims.IdentityClaims)
if userID == "" {
err = konnectoidc.NewOAuth2Error(oidc.ErrorCodeOAuth2InvalidToken, "missing data in kc.identity claim")
goto done
}
ctx = konnect.NewClaimsContext(ctx, claims)
currentIdentityManager, claimsErr := p.getIdentityManagerFromClaims(claims.IdentityProvider, claims.IdentityClaims)
if claimsErr != nil {
err = claimsErr
goto done
}
// Lookup Ref values from backend.
approvedScopes, err = currentIdentityManager.ApprovedScopes(ctx, claims.Subject, tr.ClientID, claims.Ref)
if err != nil {
goto done
}
if approvedScopes == nil {
// Use approvals from token if backend did not say anything.
approvedScopes = make(map[string]bool)
for _, scope := range claims.ApprovedScopesList {
approvedScopes[scope] = true
}
}
if len(tr.Scopes) > 0 {
// Make sure all requested scopes are granted and limit authorized
// scopes to the requested scopes.
authorizedScopes = make(map[string]bool)
for scope := range tr.Scopes {
if !approvedScopes[scope] {
err = konnectoidc.NewOAuth2Error(oidc.ErrorCodeOAuth2InsufficientScope, "insufficient scope")
goto done
} else {
authorizedScopes[scope] = true
}
}
} else {
// Authorize all approved scopes when no scopes are in request.
authorizedScopes = approvedScopes
}
// Load user record from identitymanager, without any scopes or claims.
auth, found, err = currentIdentityManager.Fetch(ctx, userID, sessionRef, nil, nil, authorizedScopes)
if !found {
err = konnectoidc.NewOAuth2Error(oidc.ErrorCodeOAuth2InvalidGrant, "user not found")
goto done
}
if err != nil {
goto done
}
// Add authorized scopes.
auth.AuthorizeScopes(authorizedScopes)
// Add authorized claims from request.
auth.AuthorizeClaims(claims.ApprovedClaimsRequest)
// Create fake request for token generation.
ar = &payload.AuthenticationRequest{
ClientID: claims.Audience[0],
}
// Remember refresh token claims, for use in access and id token generators later on.
refreshTokenClaims = claims
default:
err = konnectoidc.NewOAuth2Error(oidc.ErrorCodeOAuth2UnsupportedGrantType, "grant_type value not implemented")
goto done
}
// Create access token.
accessTokenString, err = p.makeAccessToken(ctx, ar.ClientID, auth, signinMethod, refreshTokenClaims)
if err != nil {
goto done
}
switch tr.GrantType {
case oidc.GrantTypeAuthorizationCode, oidc.GrantTypeRefreshToken:
// Create ID token when not previously requested amd openid scope is authorized.
if !ar.ResponseTypes[oidc.ResponseTypeIDToken] && authorizedScopes[oidc.ScopeOpenID] {
idTokenString, err = p.makeIDToken(ctx, ar, auth, session, accessTokenString, "", signinMethod, refreshTokenClaims)
if err != nil {
goto done
}
}
// Create refresh token when granted.
if authorizedScopes[oidc.ScopeOfflineAccess] {
refreshTokenString, err = p.makeRefreshToken(ctx, ar.ClientID, auth, nil)
if err != nil {
goto done
}
}
}
done:
if err != nil {
switch err.(type) {
case *konnectoidc.OAuth2Error:
err = utils.WriteJSON(rw, http.StatusBadRequest, err, "")
if err != nil {
p.logger.WithError(err).Errorln("token request failed writing response")
return
}
default:
p.logger.WithFields(utils.ErrorAsFields(err)).Errorln("token request failed")
p.ErrorPage(rw, http.StatusInternalServerError, err.Error(), "well sorry, but there was a problem")
}
return
}
// Successful Token Response
// http://openid.net/specs/openid-connect-core-1_0.html#TokenResponse
response := &payload.TokenSuccess{}
if accessTokenString != "" {
response.AccessToken = accessTokenString
response.TokenType = oidc.TokenTypeBearer
response.ExpiresIn = int64(p.accessTokenDuration.Seconds())
}
if idTokenString != "" {
response.IDToken = idTokenString
}
if refreshTokenString != "" {
response.RefreshToken = refreshTokenString
}
err = utils.WriteJSON(rw, http.StatusOK, response, "")
if err != nil {
p.logger.WithError(err).Errorln("token request failed writing response")
}
}
// UserInfoHandler implements the HTTP userinfo endpoint for OpenID
// Connect 1.0 as specified at https://openid.net/specs/openid-connect-core-1_0.html#UserInfo
func (p *Provider) UserInfoHandler(rw http.ResponseWriter, req *http.Request) {
var err error
addResponseHeaders(rw.Header())
switch req.Method {
case http.MethodHead:
fallthrough
case http.MethodPost:
fallthrough
case http.MethodGet:
// pass
default:
return
}
// Parse and validate UserInfo request
// https://openid.net/specs/openid-connect-core-1_0.html#UserInfoRequest
claims, err := p.GetAccessTokenClaimsFromRequest(req)
if err != nil {
p.logger.WithFields(utils.ErrorAsFields(err)).Debugln("userinfo request unauthorized")
konnectoidc.WriteWWWAuthenticateError(rw, http.StatusUnauthorized, err)
return
}
var auth identity.AuthRecord
var found bool
var requestedClaimsMap []*payload.ClaimsRequestMap
var authorizedScopes map[string]bool
userID, sessionRef := p.getUserIDAndSessionRefFromClaims(&claims.RegisteredClaims, claims.SessionClaims, claims.IdentityClaims)
ctx := konnect.NewClaimsContext(konnect.NewRequestContext(req.Context(), req), claims)
currentIdentityManager, err := p.getIdentityManagerFromClaims(claims.IdentityProvider, claims.IdentityClaims)
if err != nil {
goto done
}
if userID == "" {
err = fmt.Errorf("missing data in identity claim")
goto done
}
if claims.AuthorizedClaimsRequest != nil && claims.AuthorizedClaimsRequest.UserInfo != nil {
requestedClaimsMap = []*payload.ClaimsRequestMap{claims.AuthorizedClaimsRequest.UserInfo}
}
authorizedScopes = claims.AuthorizedScopes()
auth, found, err = currentIdentityManager.Fetch(ctx, userID, sessionRef, authorizedScopes, requestedClaimsMap, authorizedScopes)
if err != nil {
p.logger.WithFields(utils.ErrorAsFields(err)).Errorln("identity manager fetch failed")
found = false
}
if !found {
p.logger.WithField("sub", claims.RegisteredClaims.Subject).Debugln("userinfo request user not found")
p.ErrorPage(rw, http.StatusNotFound, "", "user not found")
return
}
done:
if err != nil {
p.logger.WithFields(utils.ErrorAsFields(err)).Debugln("userinfo request invalid token")
konnectoidc.WriteWWWAuthenticateError(rw, http.StatusUnauthorized, konnectoidc.NewOAuth2Error(oidc.ErrorCodeOAuth2InvalidToken, err.Error()))
return
}
publicSubject, err := p.PublicSubjectFromAuth(auth)
if err != nil {
p.logger.WithFields(utils.ErrorAsFields(err)).Debugln("userinfo request failed to create subject")
p.ErrorPage(rw, http.StatusInternalServerError, "", err.Error())
return
}
response := &konnect.UserInfoResponse{
UserInfoResponse: &payload.UserInfoResponse{
UserInfoClaims: konnectoidc.UserInfoClaims{
Subject: publicSubject,
},
ProfileClaims: konnectoidc.NewProfileClaims(auth.Claims(oidc.ScopeProfile)[0]),
EmailClaims: konnectoidc.NewEmailClaims(auth.Claims(oidc.ScopeEmail)[0]),
},
}
// Helper to receive user from auth, but only once.
withUser := func() func() identity.User {
var u identity.User
var fetched bool
return func() identity.User {
if !fetched {
fetched = true
u = auth.User()
}
return u
}
}()
authorizedScopes = auth.AuthorizedScopes()
var user identity.User
// Include additional Konnect specific claims when corresponding scopes are authorized.
if ok, _ := authorizedScopes[konnect.ScopeNumericID]; ok {
user = withUser()
if userWithID, ok := user.(identity.UserWithID); ok {
claims := &konnect.NumericIDClaims{
NumericID: userWithID.ID(),
}
if userWithUsername, ok := user.(identity.UserWithUsername); ok {
claims.NumericIDUsername = userWithUsername.Username()
}
if claims.NumericIDUsername == "" {
claims.NumericIDUsername = user.Subject()
}
response.NumericIDClaims = claims
}
}
if ok, _ := authorizedScopes[konnect.ScopeUniqueUserID]; ok {
user = withUser()
if userWithUniqueID, ok := user.(identity.UserWithUniqueID); ok {
claims := &konnect.UniqueUserIDClaims{
UniqueUserID: userWithUniqueID.UniqueID(),
}
response.UniqueUserIDClaims = claims
}
}
// Create a map so additional user specific claims can be added.
responseAsMap, err := payload.ToMap(response)
if err != nil {
p.logger.WithFields(utils.ErrorAsFields(err)).Debugln("userinfo request failed to encode claims")
p.ErrorPage(rw, http.StatusInternalServerError, "", err.Error())
return
}
// Inject extra claims.
extraClaims := auth.Claims(konnect.InternalExtraAccessTokenClaimsClaim)[0]
if extraClaims != nil {
if extraClaimsMap, ok := extraClaims.(jwt.MapClaims); ok {
for claim, value := range extraClaimsMap {
responseAsMap[claim] = value
}
}
}
// Support returning signed user info if the registered client requested it
// as specified in https://openid.net/specs/openid-connect-core-1_0.html#UserInfoResponse and
// https://openid.net/specs/openid-connect-registration-1_0.html#ClientMetadata
registration, _ := p.clients.Get(ctx, claims.Audience[0])
if registration != nil {
if registration.RawUserInfoSignedResponseAlg != "" {
// Get alg.
alg := jwt.GetSigningMethod(registration.RawUserInfoSignedResponseAlg)
// Set extra claims.
responseAsMap[oidc.IssuerIdentifierClaim] = p.issuerIdentifier
responseAsMap[oidc.AudienceClaim] = registration.ID
tokenString, err := p.makeJWT(ctx, alg, jwt.MapClaims(responseAsMap))
if err != nil {
p.logger.WithFields(utils.ErrorAsFields(err)).Debugln("userinfo request failed to encode jwt")
p.ErrorPage(rw, http.StatusInternalServerError, "", err.Error())
return
}
rw.Header().Set("Content-Type", "application/jwt")
rw.Write([]byte(tokenString))
return
}
}
err = utils.WriteJSON(rw, http.StatusOK, responseAsMap, "")
if err != nil {
p.logger.WithError(err).Errorln("userinfo request failed writing response")
}
}
// EndSessionHandler implements the HTTP endpoint for RP initiated logout with
// OpenID Connect Session Management 1.0 as specified at
// https://openid.net/specs/openid-connect-session-1_0.html#RPLogout
func (p *Provider) EndSessionHandler(rw http.ResponseWriter, req *http.Request) {
var err error
var session *payload.Session
var currentIdentityManager identity.Manager
addResponseHeaders(rw.Header())
ctx := konnect.NewRequestContext(req.Context(), req)
// Validate request.
err = req.ParseForm()
if err != nil {
p.logger.WithError(err).Errorln("endsession request invalid form data")
p.ErrorPage(rw, http.StatusBadRequest, oidc.ErrorCodeOAuth2InvalidRequest, err.Error())
return
}
esr, err := payload.DecodeEndSessionRequest(req, p.metadata)
if err != nil {
p.logger.WithError(err).Errorln("endsession request invalid request data")
p.ErrorPage(rw, http.StatusBadRequest, oidc.ErrorCodeOAuth2InvalidRequest, err.Error())
return
}
err = esr.Validate(func(token *jwt.Token) (interface{}, error) {
// Validator for incoming IDToken hints, looks up key.
return p.validateJWT(token)
})
if err != nil {
goto done
}
// Get our session.
session, err = p.getSession(req)
if err != nil {
goto done
}
currentIdentityManager, err = p.getIdentityManagerFromSession(session)
if err != nil {
goto done
}
// Authorization unauthenticates end user.
err = currentIdentityManager.EndSession(ctx, rw, req, esr)
if err != nil {
goto done
}
done:
if err != nil {
switch err.(type) {
case *payload.AuthenticationBadRequest:
p.ErrorPage(rw, http.StatusBadRequest, err.Error(), err.(*payload.AuthenticationBadRequest).Description())
case *identity.RedirectError:
p.Found(rw, err.(*identity.RedirectError).RedirectURI(), nil, false)
case *identity.IsHandledError:
// do nothing
case *konnectoidc.OAuth2Error:
err = esr.NewError(err.Error(), err.(*konnectoidc.OAuth2Error).Description())
uri := esr.MakeRedirectEndSessionRequestURL()
if uri == nil {
p.ErrorPage(rw, http.StatusForbidden, err.Error(), "oauth2 error")
} else {
p.Found(rw, uri, err, false)
}
default:
p.logger.WithFields(utils.ErrorAsFields(err)).Errorln("endsession request failed")
p.ErrorPage(rw, http.StatusInternalServerError, err.Error(), "well sorry, but there was a problem")
}
return
}
// EndSession Response.
response := &payload.AuthenticationSuccess{
State: esr.State,
}
uri := esr.MakeRedirectEndSessionRequestURL()
if uri == nil {
err = utils.WriteJSON(rw, http.StatusOK, response, "")
if err != nil {
p.logger.WithError(err).Errorln("endsession request failed writing response")
}
} else {
p.Found(rw, uri, nil, false)
}
}
// CheckSessionIframeHandler implements the HTTP endpoint for OP iframe with
// OpenID Connect Session Management 1.0 as specified at
// https://openid.net/specs/openid-connect-session-1_0.html#OPiframe
func (p *Provider) CheckSessionIframeHandler(rw http.ResponseWriter, req *http.Request) {
addResponseHeaders(rw.Header())
nonce := rndm.GenerateRandomString(32)
rw.Header().Set("Content-Type", "text/html; charset=utf-8")
rw.Header().Set("X-XSS-Protection", "1; mode=block")
rw.Header().Set("Content-Security-Policy", fmt.Sprintf("default-src 'none'; script-src 'nonce-%s'", nonce))
data := struct {
CookieName string
Nonce string
}{
CookieName: p.browserStateCookieName,
Nonce: nonce,
}
checkSessionIframeTemplate.Execute(rw, data)
}
// RegistrationHandler implements the HTTP endpoint for client self registration
// with OpenID Connect Registration 1.0 as specified at
// https://openid.net/specs/openid-connect-registration-1_0.html#ClientRegistration
func (p *Provider) RegistrationHandler(rw http.ResponseWriter, req *http.Request) {
req.Body = http.MaxBytesReader(rw, req.Body, registrationSizeLimit)
addResponseHeaders(rw.Header())
crr, err := payload.DecodeClientRegistrationRequest(req)
if err != nil {
p.logger.WithError(err).Errorln("client registration request failed to decode request data")
p.ErrorPage(rw, http.StatusBadRequest, oidc.ErrorCodeOAuth2InvalidRequest, err.Error())
return
}
var cr *clients.ClientRegistration
// Validate request method
switch req.Method {
case http.MethodPost:
// breaks
default:
err = konnectoidc.NewOAuth2Error(oidc.ErrorCodeOAuth2InvalidRequest, "request must be sent with POST")
goto done
}
// Validate request.
err = crr.Validate()
if err != nil {
goto done
}
// Get registration record.
cr, err = crr.ClientRegistration()
if err != nil {
goto done
}
// Set client to dynamic. This creates the id and client secret.
err = cr.SetDynamic(clients.NewRegistryContext(req.Context(), p.clients), p.clients.StatelessCreator)
if err != nil {
goto done
}
done:
if err != nil {
switch err.(type) {
case *konnectoidc.OAuth2Error:
err = utils.WriteJSON(rw, http.StatusBadRequest, err, "")
if err != nil {
p.logger.WithError(err).Errorln("client registration request failed writing response")
return
}
default:
p.logger.WithFields(utils.ErrorAsFields(err)).Errorln("client registration request failed")
p.ErrorPage(rw, http.StatusInternalServerError, err.Error(), "well sorry, but there was a problem")
}
return
}
p.logger.WithFields(logrus.Fields{
"client_id": cr.ID,
"name": cr.Name,
"application_type": cr.ApplicationType,
"redirect_uris": cr.RedirectURIs,
}).Debugln("registered dynamic client")
response := &payload.ClientRegistrationResponse{
ClientID: cr.ID,
ClientSecret: cr.Secret,
ClientIDIssuedAt: cr.IDIssuedAt.Unix(),
ClientSecretExpiresAt: cr.SecretExpiresAt.Unix(),
ClientRegistrationRequest: *crr,
}
err = utils.WriteJSON(rw, http.StatusCreated, response, "")
if err != nil {
p.logger.WithError(err).Errorln("client registration request failed writing response")
}
}