Initial QSfera import

This commit is contained in:
Курнат Андрей
2026-06-07 10:20:04 +03:00
commit 2315f25754
16485 changed files with 4826827 additions and 0 deletions
+52
View File
@@ -0,0 +1,52 @@
/*
* Copyright 2017-2019 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 (
"net/http"
"time"
"github.com/libregraph/lico/config"
)
// Config defines a Provider's configuration settings.
type Config struct {
Config *config.Config
IssuerIdentifier string
WellKnownPath string
JwksPath string
AuthorizationPath string
TokenPath string
UserInfoPath string
EndSessionPath string
CheckSessionIframePath string
RegistrationPath string
BrowserStateCookiePath string
BrowserStateCookieName string
BrowserStateCookieSameSite http.SameSite
SessionCookiePath string
SessionCookieName string
SessionCookieSameSite http.SameSite
AccessTokenDuration time.Duration
IDTokenDuration time.Duration
RefreshTokenDuration time.Duration
}
+93
View File
@@ -0,0 +1,93 @@
/*
* Copyright 2017-2019 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 (
"net/http"
)
func (p *Provider) setBrowserStateCookie(rw http.ResponseWriter, value string) error {
cookie := http.Cookie{
Name: p.browserStateCookieName,
Value: value,
Path: p.browserStateCookiePath,
Secure: true,
HttpOnly: false, // This Cookie is intended to be read by Javascript.
SameSite: p.browserStateCookieSameSite,
}
http.SetCookie(rw, &cookie)
return nil
}
func (p *Provider) removeBrowserStateCookie(rw http.ResponseWriter) error {
cookie := http.Cookie{
Name: p.browserStateCookieName,
Path: p.browserStateCookiePath,
Secure: true,
HttpOnly: false, // This Cookie is intended to be read by Javascript.
SameSite: p.browserStateCookieSameSite,
Expires: farPastExpiryTime,
}
http.SetCookie(rw, &cookie)
return nil
}
func (p *Provider) setSessionCookie(rw http.ResponseWriter, value string) error {
cookie := http.Cookie{
Name: p.sessionCookieName,
Value: value,
Path: p.sessionCookiePath,
Secure: true,
HttpOnly: true,
SameSite: p.sessionCookieSameSite,
}
http.SetCookie(rw, &cookie)
return nil
}
func (p *Provider) getSessionCookie(req *http.Request) (string, error) {
cookie, err := req.Cookie(p.sessionCookieName)
if err != nil {
return "", err
}
return cookie.Value, nil
}
func (p *Provider) removeSessionCookie(rw http.ResponseWriter) error {
cookie := http.Cookie{
Name: p.sessionCookieName,
Path: p.sessionCookiePath,
Secure: true,
HttpOnly: true,
SameSite: p.sessionCookieSameSite,
Expires: farPastExpiryTime,
}
http.SetCookie(rw, &cookie)
return nil
}
+947
View File
@@ -0,0 +1,947 @@
/*
* 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")
}
}
+201
View File
@@ -0,0 +1,201 @@
/*
* Copyright 2017-2019 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 (
"html/template"
)
var checkSessionIframeTemplate = template.Must(template.New("check-session.html").Parse(`
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<script id="cookie-name" type="application/x-kkbs-name">{{.CookieName}}</script>
<script type="text/javascript" nonce={{.Nonce}}>
/*
Forge-SHA256 - https://github.com/brillout/forge-sha256/tree/6ad5535e0be2385fdc53f1d9ce2b172365c70333
The MIT License (MIT)
Copyright (c) 2015-2017 Romuald Brillout and contributors
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
Forge project - https://github.com/digitalbazaar/forge
New BSD License (3-clause)
Copyright (c) 2010, Digital Bazaar, Inc.
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.
* Neither the name of Digital Bazaar, Inc. nor the
names of its contributors may be used to endorse or promote products
derived from this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL DIGITAL BAZAAR BE LIABLE FOR ANY
DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
(function(){function p(a){this.data="";this.a=0;if("string"===typeof a)this.data=a;else if(b.D(a)||b.L(a)){a=new Uint8Array(a);try{this.data=String.fromCharCode.apply(null,a)}catch(f){for(var v=0;v<a.length;++v)this.M(a[v])}}else if(a instanceof p||"object"===typeof a&&"string"===typeof a.data&&"number"===typeof a.a)this.data=a.data,this.a=a.a;this.v=0}function w(a,f,b){for(var d,c,h,m,g,k,e,r,n,l,t,q,u,p=b.length();64<=p;){for(g=0;16>g;++g)f[g]=b.getInt32();for(;64>g;++g)d=f[g-2],d=(d>>>17|d<<15)^
(d>>>19|d<<13)^d>>>10,c=f[g-15],c=(c>>>7|c<<25)^(c>>>18|c<<14)^c>>>3,f[g]=d+f[g-7]+c+f[g-16]|0;k=a.g;e=a.h;r=a.i;n=a.j;l=a.l;t=a.m;q=a.o;u=a.s;for(g=0;64>g;++g)d=(l>>>6|l<<26)^(l>>>11|l<<21)^(l>>>25|l<<7),h=q^l&(t^q),c=(k>>>2|k<<30)^(k>>>13|k<<19)^(k>>>22|k<<10),m=k&e|r&(k^e),d=u+d+h+x[g]+f[g],c+=m,u=q,q=t,t=l,l=n+d|0,n=r,r=e,e=k,k=d+c|0;a.g=a.g+k|0;a.h=a.h+e|0;a.i=a.i+r|0;a.j=a.j+n|0;a.l=a.l+l|0;a.m=a.m+t|0;a.o=a.o+q|0;a.s=a.s+u|0;p-=64}}var m,y,e,b=m=m||{};b.D=function(a){return"undefined"!==typeof ArrayBuffer&&
a instanceof ArrayBuffer};b.L=function(a){return a&&b.D(a.buffer)&&void 0!==a.byteLength};b.G=p;b.b=p;b.b.prototype.H=function(a){this.v+=a;4096<this.v&&(this.v=0)};b.b.prototype.length=function(){return this.data.length-this.a};b.b.prototype.M=function(a){this.u(String.fromCharCode(a))};b.b.prototype.u=function(a){this.data+=a;this.H(a.length)};b.b.prototype.c=function(a){this.u(String.fromCharCode(a>>24&255)+String.fromCharCode(a>>16&255)+String.fromCharCode(a>>8&255)+String.fromCharCode(a&255))};
b.b.prototype.getInt16=function(){var a=this.data.charCodeAt(this.a)<<8^this.data.charCodeAt(this.a+1);this.a+=2;return a};b.b.prototype.getInt32=function(){var a=this.data.charCodeAt(this.a)<<24^this.data.charCodeAt(this.a+1)<<16^this.data.charCodeAt(this.a+2)<<8^this.data.charCodeAt(this.a+3);this.a+=4;return a};b.b.prototype.B=function(){return this.data.slice(this.a)};b.b.prototype.compact=function(){0<this.a&&(this.data=this.data.slice(this.a),this.a=0);return this};b.b.prototype.clear=function(){this.data=
"";this.a=0;return this};b.b.prototype.truncate=function(a){a=Math.max(0,this.length()-a);this.data=this.data.substr(this.a,a);this.a=0;return this};b.b.prototype.N=function(){for(var a="",f=this.a;f<this.data.length;++f){var b=this.data.charCodeAt(f);16>b&&(a+="0");a+=b.toString(16)}return a};b.b.prototype.toString=function(){return b.I(this.B())};b.createBuffer=function(a,f){void 0!==a&&"utf8"===(f||"raw")&&(a=b.C(a));return new b.G(a)};b.J=function(){for(var a=String.fromCharCode(0),b=64,e="";0<
b;)b&1&&(e+=a),b>>>=1,0<b&&(a+=a);return e};b.C=function(a){return unescape(encodeURIComponent(a))};b.I=function(a){return decodeURIComponent(escape(a))};b.K=function(a){for(var b=0;b<a.length;b++)if(a.charCodeAt(b)>>>8)return!0;return!1};var z=y=y||{};e=e||{};e.A=e.A||{};e.F=e.A.F=z;z.create=function(){A||(n=String.fromCharCode(128),n+=m.J(),x=[1116352408,1899447441,3049323471,3921009573,961987163,1508970993,2453635748,2870763221,3624381080,310598401,607225278,1426881987,1925078388,2162078206,2614888103,
3248222580,3835390401,4022224774,264347078,604807628,770255983,1249150122,1555081692,1996064986,2554220882,2821834349,2952996808,3210313671,3336571891,3584528711,113926993,338241895,666307205,773529912,1294757372,1396182291,1695183700,1986661051,2177026350,2456956037,2730485921,2820302411,3259730800,3345764771,3516065817,3600352804,4094571909,275423344,430227734,506948616,659060556,883997877,958139571,1322822218,1537002063,1747873779,1955562222,2024104815,2227730452,2361852424,2428436474,2756734187,
3204031479,3329325298],A=!0);var a=null,b=m.createBuffer(),e=Array(64),d={algorithm:"sha256",O:64,P:32,w:0,f:[0,0],start:function(){d.w=0;d.f=[0,0];b=m.createBuffer();a={g:1779033703,h:3144134277,i:1013904242,j:2773480762,l:1359893119,m:2600822924,o:528734635,s:1541459225};return d}};d.start();d.update=function(c,h){"utf8"===h&&(c=m.C(c));d.w+=c.length;d.f[0]+=c.length/4294967296>>>0;d.f[1]+=c.length>>>0;b.u(c);w(a,e,b);(2048<b.a||0===b.length())&&b.compact();return d};d.digest=function(){var c=m.createBuffer();
c.u(b.B());c.u(n.substr(0,64-(d.f[1]+8&63)));c.c(d.f[0]<<3|d.f[0]>>>28);c.c(d.f[1]<<3);var h={g:a.g,h:a.h,i:a.i,j:a.j,l:a.l,m:a.m,o:a.o,s:a.s};w(h,e,c);c=m.createBuffer();c.c(h.g);c.c(h.h);c.c(h.i);c.c(h.j);c.c(h.l);c.c(h.m);c.c(h.o);c.c(h.s);return c};return d};var n=null,A=!1,x=null;window.forge_sha256=function(a){var f=e.F.create();f.update(a,b.K(a)?"utf8":void 0);return f.digest().N()}})();
</script>
</head>
<body>
<noscript>
You need to enable JavaScript to run this app.
</noscript>
<script type="text/javascript" nonce={{.Nonce}}>
// This implements OpenID Connect Session Managament 1.0 OP IFrame as specified
// in https://openid.net/specs/openid-connect-session-1_0.html#OPiframe
(function() {
var cache = {};
// Get cookie name.
var cookieNameElem = document.getElementById('cookie-name');
if (cookieNameElem) {
var cookieName = cookieNameElem.textContent.trim();
}
// Helper to get cookie by name.
function getCookie(name) {
var value = '; ' + document.cookie;
var parts = value.split('; ' + name + "=");
if (parts.length === 2) {
return parts.pop().split(';').shift();
}
}
function getBrowserState() {
var browserState = getCookie(cookieName);
return browserState ? browserState : '';
}
function makeSessionState(clientID, origin, browserState, salt) {
return ''+forge_sha256(clientID + ' ' + origin + ' ' + browserState + ' ' + salt);
}
// Outer error catching data processor.
function receiveMessage(origin, data) {
if (!origin || !data) {
return 'error';
}
try {
return handleMessage(origin, data);
} catch (err) {
return 'error';
}
}
// Parse and validatie.
function handleMessage(origin, data) {
// Parse data.
var dataParts = data.split(' ');
if (dataParts.length !== 2) {
return 'error';
}
var clientID = dataParts[0];
var sessionState = dataParts[1];
if (!clientID || !sessionState) {
return 'error';
}
var sessionStateParts = sessionState.split('.');
if (sessionStateParts.length != 2) {
return 'error';
}
var clientStateHash = sessionStateParts[0];
var salt = sessionStateParts[1];
if (!clientStateHash || !salt) {
return 'error';
}
// Get browser state.
var browserState = getBrowserState();
// Make session state.
var expectedStateHash = makeSessionState(clientID, origin, browserState, salt);
// Compare.
var status = clientStateHash === expectedStateHash ? 'unchanged' : 'changed';
// Cache.
if (cache.browserState === browserState && status === 'changed' && cache.status === status) {
// If the browser state remains unchanged, something with the cookie
// did not work properly. To avoid a fast repeating loop, it is
// better to fail with error.
return 'error';
}
cache.status = status;
cache.browserState = browserState;
return status;
}
// Register event.
if (cookieName && window.parent !== window) {
window.addEventListener('message', function(event) {
// Only do something when receiving a message from our parent or
// from another window which shares our parent.
if (window.parent === event.source || (window !== event.source && window.parent === event.source.parent)) {
var response = receiveMessage(event.origin, event.data);
event.source.postMessage(response, event.origin);
}
}, false);
}
})();
</script>
</body>
</html>
`))
+61
View File
@@ -0,0 +1,61 @@
/*
* Copyright 2017-2019 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 (
"errors"
"github.com/golang-jwt/jwt/v5"
"github.com/libregraph/lico/identity"
"github.com/libregraph/lico/oidc/payload"
)
func (p *Provider) getIdentityManager(identityProvider string) (identity.Manager, error) {
if identityProvider == "" {
// Return default manager when empty (backwards compatibility).
return p.identityManager, nil
}
if identityProvider == p.identityManager.Name() {
return p.identityManager, nil
}
if p.guestManager != nil && identityProvider == p.guestManager.Name() {
return p.guestManager, nil
}
return nil, errors.New("identity provider mismatch")
}
func (p *Provider) getIdentityManagerFromClaims(identityProvider string, identityClaims jwt.MapClaims) (identity.Manager, error) {
if identityClaims == nil {
// Return default manager when no claims.
return p.identityManager, nil
}
return p.getIdentityManager(identityProvider)
}
func (p *Provider) getIdentityManagerFromSession(session *payload.Session) (identity.Manager, error) {
if session == nil {
// Return default manager when no session.
return p.identityManager, nil
}
return p.getIdentityManager(session.Provider)
}
+533
View File
@@ -0,0 +1,533 @@
/*
* Copyright 2017-2019 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"
"crypto"
"crypto/ecdsa"
"crypto/rsa"
"crypto/x509"
"fmt"
"net/http"
"net/url"
"strings"
"time"
"github.com/golang-jwt/jwt/v5"
"github.com/libregraph/oidc-go"
"github.com/rs/cors"
"github.com/sirupsen/logrus"
"golang.org/x/crypto/ed25519"
konnect "github.com/libregraph/lico"
"github.com/libregraph/lico/identity"
"github.com/libregraph/lico/identity/clients"
identityManagers "github.com/libregraph/lico/identity/managers"
"github.com/libregraph/lico/managers"
konnectoidc "github.com/libregraph/lico/oidc"
"github.com/libregraph/lico/oidc/code"
"github.com/libregraph/lico/utils"
)
// Provider defines an OIDC provider with the handlers for the OIDC endpoints.
type Provider struct {
Config *Config
issuerIdentifier string
metadata *oidc.WellKnown
wellKnownPath string
jwksPath string
authorizationPath string
tokenPath string
userInfoPath string
endSessionPath string
checkSessionIframePath string
registrationPath string
identityManager identity.Manager
guestManager identity.Manager
codeManager code.Manager
encryptionManager *identityManagers.EncryptionManager
clients *clients.Registry
signingKeys map[jwt.SigningMethod]*SigningKey
signingMethodDefault jwt.SigningMethod
validationKeys map[string]crypto.PublicKey
certificates map[string][]*x509.Certificate
browserStateCookiePath string
browserStateCookieName string
browserStateCookieSameSite http.SameSite
sessionCookiePath string
sessionCookieName string
sessionCookieSameSite http.SameSite
accessTokenDuration time.Duration
idTokenDuration time.Duration
refreshTokenDuration time.Duration
logger logrus.FieldLogger
}
// NewProvider returns a new Provider.
func NewProvider(c *Config) (*Provider, error) {
p := &Provider{
Config: c,
issuerIdentifier: c.IssuerIdentifier,
wellKnownPath: c.WellKnownPath,
jwksPath: c.JwksPath,
authorizationPath: c.AuthorizationPath,
tokenPath: c.TokenPath,
userInfoPath: c.UserInfoPath,
endSessionPath: c.EndSessionPath,
checkSessionIframePath: c.CheckSessionIframePath,
registrationPath: c.RegistrationPath,
signingKeys: make(map[jwt.SigningMethod]*SigningKey),
validationKeys: make(map[string]crypto.PublicKey),
certificates: make(map[string][]*x509.Certificate),
browserStateCookiePath: c.BrowserStateCookiePath,
browserStateCookieName: c.BrowserStateCookieName,
browserStateCookieSameSite: c.BrowserStateCookieSameSite,
sessionCookiePath: c.SessionCookiePath,
sessionCookieName: c.SessionCookieName,
sessionCookieSameSite: c.SessionCookieSameSite,
accessTokenDuration: c.AccessTokenDuration,
idTokenDuration: c.IDTokenDuration,
refreshTokenDuration: c.RefreshTokenDuration,
logger: c.Config.Logger,
}
return p, nil
}
// RegisterManagers registers the provided managers from the
func (p *Provider) RegisterManagers(mgrs *managers.Managers) error {
p.identityManager = mgrs.Must("identity").(identity.Manager)
p.codeManager = mgrs.Must("code").(code.Manager)
p.encryptionManager = mgrs.Must("encryption").(*identityManagers.EncryptionManager)
p.clients = mgrs.Must("clients").(*clients.Registry)
// Register callback to cleanup our cookie whenever the identity is unset or
// set.
onSetLogon := func(ctx context.Context, rw http.ResponseWriter, user identity.User) error {
// NOTE(longsleep): This leaves room for optionmization. In theory it
// should be possible to set the new browser state here directly since
// the relevant information should be available in the identity manager
// and thus avoiding a potentially unset refresh and client side
// redirects whenever the same user signs in again.
return p.removeBrowserStateCookie(rw)
}
onUnsetLogon := func(ctx context.Context, rw http.ResponseWriter) error {
var err error
// Remove browser state cookie.
if errBsc := p.removeBrowserStateCookie(rw); errBsc != nil {
err = errBsc
}
// Remove OIDC client session cookie.
if errSc := p.removeSessionCookie(rw); errSc != nil {
err = errSc
}
return err
}
p.identityManager.OnSetLogon(onSetLogon)
p.identityManager.OnUnsetLogon(onUnsetLogon)
// Add guest manager if any can be found.
if guestManager, _ := mgrs.Get("guest"); guestManager != nil {
p.guestManager = guestManager.(identity.Manager)
p.guestManager.OnSetLogon(onSetLogon)
p.guestManager.OnUnsetLogon(onUnsetLogon)
}
if p.Config.RegistrationPath != "" {
// NOTE(longsleep): This is hackish. Find a better way to propagate our
// provides JWT stuff to the client registry.
p.clients.StatelessCreator = p.makeJWT
p.clients.StatelessValidator = p.validateJWT
}
return nil
}
func (p *Provider) makeIssURL(path string) string {
if path == "" {
return ""
}
u, _ := url.Parse(p.issuerIdentifier)
u.Path = "" // Strip path from issuer, whatever prefix must already be applied.
return fmt.Sprintf("%s%s", u.String(), path)
}
// SetSigningMethod sets the provided signing method as default signing method
// of the associated provider.
func (p *Provider) SetSigningMethod(signingMethod jwt.SigningMethod) error {
p.logger.WithField("alg", signingMethod.Alg()).Infoln("set provider signing alg")
p.signingMethodDefault = signingMethod
return nil
}
// SetSigningKey sets the provided signer as key for token signing with the
// provided id as key id. The public key of the provided signer is also added
// as validation key with the same key id.
func (p *Provider) SetSigningKey(id string, key crypto.Signer) error {
var signingMethod jwt.SigningMethod
// Auto select signingMethod based on the signer.
switch s := key.(type) {
case *rsa.PrivateKey:
signingMethod = jwt.SigningMethodPS256
case *ecdsa.PrivateKey:
signingMethod = jwt.SigningMethodES256
case ed25519.PrivateKey:
signingMethod = jwt.SigningMethodEdDSA
default:
return fmt.Errorf("unsupported signer type: %v", s)
}
if p.signingMethodDefault == nil {
if err := p.SetSigningMethod(signingMethod); err != nil {
return err
}
}
p.logger.WithFields(logrus.Fields{
"type": fmt.Sprintf("%T", key),
"id": id,
"method": fmt.Sprintf("%T", signingMethod),
}).Infoln("set provider signing key")
switch signingMethod.(type) {
case *jwt.SigningMethodECDSA:
// Add all other supported ECDSA signing methods as well.
p.signingKeys[jwt.SigningMethodES256] = &SigningKey{
ID: id,
PrivateKey: key,
SigningMethod: jwt.SigningMethodES256,
}
p.signingKeys[jwt.SigningMethodES384] = &SigningKey{
ID: id,
PrivateKey: key,
SigningMethod: jwt.SigningMethodES384,
}
p.signingKeys[jwt.SigningMethodES512] = &SigningKey{
ID: id,
PrivateKey: key,
SigningMethod: jwt.SigningMethodES512,
}
case *jwt.SigningMethodRSA:
// Add all supported RSA and RSAPSS signing methods as well.
p.signingKeys[jwt.SigningMethodRS256] = &SigningKey{
ID: id,
PrivateKey: key,
SigningMethod: jwt.SigningMethodRS256,
}
p.signingKeys[jwt.SigningMethodRS384] = &SigningKey{
ID: id,
PrivateKey: key,
SigningMethod: jwt.SigningMethodRS384,
}
p.signingKeys[jwt.SigningMethodRS512] = &SigningKey{
ID: id,
PrivateKey: key,
SigningMethod: jwt.SigningMethodRS512,
}
p.signingKeys[jwt.SigningMethodPS256] = &SigningKey{
ID: id,
PrivateKey: key,
SigningMethod: jwt.SigningMethodPS256,
}
p.signingKeys[jwt.SigningMethodPS384] = &SigningKey{
ID: id,
PrivateKey: key,
SigningMethod: jwt.SigningMethodPS384,
}
p.signingKeys[jwt.SigningMethodPS512] = &SigningKey{
ID: id,
PrivateKey: key,
SigningMethod: jwt.SigningMethodPS512,
}
case *jwt.SigningMethodRSAPSS:
// Add all supported RSA and RSAPSS signing methods as well.
p.signingKeys[jwt.SigningMethodRS256] = &SigningKey{
ID: id,
PrivateKey: key,
SigningMethod: jwt.SigningMethodRS256,
}
p.signingKeys[jwt.SigningMethodRS384] = &SigningKey{
ID: id,
PrivateKey: key,
SigningMethod: jwt.SigningMethodRS384,
}
p.signingKeys[jwt.SigningMethodRS512] = &SigningKey{
ID: id,
PrivateKey: key,
SigningMethod: jwt.SigningMethodRS512,
}
p.signingKeys[jwt.SigningMethodPS256] = &SigningKey{
ID: id,
PrivateKey: key,
SigningMethod: jwt.SigningMethodPS256,
}
p.signingKeys[jwt.SigningMethodPS384] = &SigningKey{
ID: id,
PrivateKey: key,
SigningMethod: jwt.SigningMethodPS384,
}
p.signingKeys[jwt.SigningMethodPS512] = &SigningKey{
ID: id,
PrivateKey: key,
SigningMethod: jwt.SigningMethodPS512,
}
case *jwt.SigningMethodEd25519:
p.signingKeys[signingMethod] = &SigningKey{
ID: id,
PrivateKey: key,
SigningMethod: signingMethod,
}
default:
return fmt.Errorf("unsupported signing method type")
}
if _, ok := p.signingKeys[signingMethod]; !ok {
return fmt.Errorf("unsupported signing method")
}
p.SetValidationKey(id, key.Public())
return nil
}
// GetSigningKey returns a matching signing key for the provided signing method.
func (p *Provider) GetSigningKey(signingMethod jwt.SigningMethod) (*SigningKey, bool) {
return p.getSigningKey(signingMethod)
}
func (p *Provider) getSigningKey(signingMethod jwt.SigningMethod) (*SigningKey, bool) {
if signingMethod == nil {
// Use default signign method if none given.
signingMethod = p.signingMethodDefault
}
sk, ok := p.signingKeys[signingMethod]
return sk, ok
}
// SetValidationKey sets the provider public key as validation key for token
// validation for tokens with the provided key.
func (p *Provider) SetValidationKey(id string, key crypto.PublicKey) error {
p.logger.WithFields(logrus.Fields{
"type": fmt.Sprintf("%T", key),
"id": id,
}).Infoln("set provider validation key")
p.validationKeys[id] = key
return nil
}
// GetValidationKey returns the validation key for the provided id.
func (p *Provider) GetValidationKey(id string) (crypto.PublicKey, bool) {
return p.getValidationKey(id)
}
func (p *Provider) getValidationKey(id string) (crypto.PublicKey, bool) {
vk, ok := p.validationKeys[id]
return vk, ok
}
// SetCertificate sets the provider certificate chain for a particular key.
func (p *Provider) SetCertificate(id string, certificates []*x509.Certificate) error {
p.logger.WithFields(logrus.Fields{
"chain_size": len(certificates),
"id": id,
}).Infoln("set provider certificate")
p.certificates[id] = certificates
return nil
}
// InitializeMetadata creates the accociated providers meta data document. Call
// this once all other settings at the provider have been done.
func (p *Provider) InitializeMetadata() error {
// Create well-known document.
p.metadata = &oidc.WellKnown{
Issuer: p.issuerIdentifier,
AuthorizationEndpoint: p.makeIssURL(p.authorizationPath),
TokenEndpoint: p.makeIssURL(p.tokenPath),
UserInfoEndpoint: p.makeIssURL(p.userInfoPath),
EndSessionEndpoint: p.makeIssURL(p.endSessionPath),
CheckSessionIframe: p.makeIssURL(p.checkSessionIframePath),
JwksURI: p.makeIssURL(p.jwksPath),
RegistrationEndpoint: p.makeIssURL(p.registrationPath),
ScopesSupported: uniqueStrings(append([]string{
oidc.ScopeOpenID,
}, p.identityManager.ScopesSupported(nil)...)),
ResponseTypesSupported: []string{
oidc.ResponseTypeIDTokenToken,
oidc.ResponseTypeIDToken,
oidc.ResponseTypeCodeIDToken,
oidc.ResponseTypeCodeIDTokenToken,
},
SubjectTypesSupported: []string{
oidc.SubjectIDPublic,
},
ClaimsParameterSupported: true,
ClaimsSupported: uniqueStrings(append([]string{
oidc.IssuerIdentifierClaim,
oidc.SubjectIdentifierClaim,
oidc.AudienceClaim,
oidc.ExpirationClaim,
oidc.IssuedAtClaim,
}, p.identityManager.ClaimsSupported(nil)...)),
RequestParameterSupported: true,
RequestURIParameterSupported: false,
}
p.metadata.IDTokenSigningAlgValuesSupported = make([]string, 0)
for alg := range p.signingKeys {
p.metadata.IDTokenSigningAlgValuesSupported = append(p.metadata.IDTokenSigningAlgValuesSupported, alg.Alg())
}
p.metadata.UserInfoSigningAlgValuesSupported = p.metadata.IDTokenSigningAlgValuesSupported
p.metadata.RequestObjectSigningAlgValuesSupported = []string{
jwt.SigningMethodES256.Alg(),
jwt.SigningMethodES384.Alg(),
jwt.SigningMethodES512.Alg(),
jwt.SigningMethodRS256.Alg(),
jwt.SigningMethodRS384.Alg(),
jwt.SigningMethodRS512.Alg(),
jwt.SigningMethodPS256.Alg(),
jwt.SigningMethodPS384.Alg(),
jwt.SigningMethodPS512.Alg(),
jwt.SigningMethodNone.Alg(),
jwt.SigningMethodEdDSA.Alg(),
}
p.metadata.TokenEndpointAuthMethodsSupported = []string{
oidc.AuthMethodClientSecretBasic,
oidc.AuthMethodNone,
}
p.metadata.TokenEndpointAuthSigningAlgValuesSupported = p.metadata.IDTokenSigningAlgValuesSupported
return nil
}
// ServerHTTP implements the http.HandlerFunc interface.
func (p *Provider) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
switch path := req.URL.Path; {
case path == p.wellKnownPath:
cors.Default().ServeHTTP(rw, req, p.WellKnownHandler)
case path == p.jwksPath:
cors.Default().ServeHTTP(rw, req, p.JwksHandler)
case path == p.authorizationPath:
p.AuthorizeHandler(rw, req)
case path == p.tokenPath:
cors.Default().ServeHTTP(rw, req, p.TokenHandler)
case path == p.userInfoPath:
// TODO(longsleep): Use more strict CORS.
cors.AllowAll().ServeHTTP(rw, req, p.UserInfoHandler)
case path == p.endSessionPath:
p.EndSessionHandler(rw, req)
case path == p.checkSessionIframePath:
p.CheckSessionIframeHandler(rw, req)
case path == p.registrationPath:
p.RegistrationHandler(rw, req)
default:
http.NotFound(rw, req)
}
}
// ErrorPage writes a HTML error page to the provided ResponseWriter.
func (p *Provider) ErrorPage(rw http.ResponseWriter, code int, title string, message string) {
utils.WriteErrorPage(rw, code, title, message)
}
// Found writes a HTTP 302 to the provided ResponseWriter with the appropriate
// Location header creates from the other parameters.
func (p *Provider) Found(rw http.ResponseWriter, uri *url.URL, params interface{}, asFragment bool) {
err := utils.WriteRedirect(rw, http.StatusFound, uri, params, asFragment)
if err != nil {
p.logger.WithError(err).Debugln("failed to write to response")
p.ErrorPage(rw, http.StatusInternalServerError, "", err.Error())
return
}
}
// LoginRequiredPage writes a HTTP 30 to the provided ResponseWrite with the
// URL of the provided request (set to the scheme and host of issuer) as
// continue parameter.
func (p *Provider) LoginRequiredPage(rw http.ResponseWriter, req *http.Request, uri *url.URL) {
issURI, _ := url.Parse(p.issuerIdentifier)
trusted, _ := utils.IsRequestFromTrustedSource(req, p.Config.Config.TrustedProxyIPs, p.Config.Config.TrustedProxyNets)
continueURI := getRequestURL(req, trusted)
continueURI.Scheme = issURI.Scheme
continueURI.Host = issURI.Host
uri, err := url.Parse(fmt.Sprintf("%s?continue=%s&oauth=1", uri.String(), url.QueryEscape(continueURI.String())))
if err != nil {
p.logger.WithError(err).Debugln("failed to parse sign-in URL")
p.ErrorPage(rw, http.StatusInternalServerError, "", err.Error())
return
}
p.Found(rw, uri, nil, false)
}
// GetAccessTokenClaimsFromRequest reads incoming request, validates the
// access token and returns the validated claims.
func (p *Provider) GetAccessTokenClaimsFromRequest(req *http.Request) (*konnect.AccessTokenClaims, error) {
var err error
var claims *konnect.AccessTokenClaims
auth := strings.SplitN(req.Header.Get("Authorization"), " ", 2)
switch auth[0] {
case oidc.TokenTypeBearer:
if len(auth) != 2 {
err = konnectoidc.NewOAuth2Error(oidc.ErrorCodeOAuth2InvalidRequest, "Invalid Bearer authorization header format")
break
}
claims = &konnect.AccessTokenClaims{}
_, err = jwt.ParseWithClaims(auth[1], claims, func(token *jwt.Token) (interface{}, error) {
// Validator for incoming access tokens, looks up key.
return p.validateJWT(token)
})
if err != nil {
// Wrap as OAuth2 error.
err = konnectoidc.NewOAuth2Error(oidc.ErrorCodeOAuth2InvalidToken, err.Error())
}
default:
err = konnectoidc.NewOAuth2Error(oidc.ErrorCodeOAuth2InvalidRequest, "Bearer authorization required")
}
return claims, err
}
+145
View File
@@ -0,0 +1,145 @@
/*
* Copyright 2017-2019 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 (
"bytes"
"encoding/base64"
"encoding/gob"
"net/http"
"github.com/golang-jwt/jwt/v5"
"github.com/longsleep/rndm"
konnect "github.com/libregraph/lico"
"github.com/libregraph/lico/identity"
"github.com/libregraph/lico/oidc"
"github.com/libregraph/lico/oidc/payload"
)
const sessionVersion = 2
func (p *Provider) getSession(req *http.Request) (*payload.Session, error) {
serialized, err := p.getSessionCookie(req)
switch err {
case nil:
// breaks
case http.ErrNoCookie:
return nil, nil
default:
return nil, err
}
// Decode.
return p.unserializeSession(serialized)
}
func (p *Provider) updateOrCreateSession(rw http.ResponseWriter, req *http.Request, ar *payload.AuthenticationRequest, auth identity.AuthRecord) (*payload.Session, error) {
session := ar.Session
if session != nil && session.Version == sessionVersion && session.Sub == auth.Subject() {
// Existing session with same sub.
return session, nil
}
// Create new session.
session = &payload.Session{
Version: sessionVersion,
ID: rndm.GenerateRandomString(32),
Sub: auth.Subject(),
Provider: auth.Manager().Name(),
}
serialized, err := p.serializeSession(session)
if err != nil {
return session, err
}
err = p.setSessionCookie(rw, serialized)
return session, err
}
func (p *Provider) serializeSession(session *payload.Session) (string, error) {
var b bytes.Buffer
enc := gob.NewEncoder(&b)
err := enc.Encode(session)
if err != nil {
return "", err
}
ciphertext, err := p.encryptionManager.Encrypt(b.Bytes())
if err != nil {
return "", err
}
return base64.StdEncoding.EncodeToString(ciphertext), nil
}
func (p *Provider) unserializeSession(value string) (*payload.Session, error) {
ciphertext, err := base64.StdEncoding.DecodeString(value)
if err != nil {
return nil, err
}
raw, err := p.encryptionManager.Decrypt(ciphertext)
if err != nil {
return nil, err
}
var session payload.Session
r := bytes.NewReader(raw)
dec := gob.NewDecoder(r)
err = dec.Decode(&session)
if err != nil {
return nil, err
}
return &session, nil
}
func (p *Provider) getUserIDAndSessionRefFromClaims(claims *jwt.RegisteredClaims, sessionClaims *oidc.SessionClaims, identityClaims jwt.MapClaims) (string, *string) {
if claims == nil || identityClaims == nil {
return "", nil
}
if len(claims.Audience) != 1 {
return "", nil
}
audience := claims.Audience[0]
userIDClaim, _ := identityClaims[konnect.IdentifiedUserIDClaim].(string)
if userIDClaim == "" {
return userIDClaim, nil
}
userClaim, _ := identityClaims[konnect.IdentifiedUserClaim].(string)
if userClaim == "" {
userClaim = userIDClaim
}
if sessionClaims != nil {
sessionIDClaim := sessionClaims.SessionID
if sessionIDClaim != "" {
return userIDClaim, &sessionIDClaim
}
}
// NOTE(longsleep): Return the userID from claims and generate a session ref
// for it. Session refs use the userClaim if available and set by the
// underlaying backend.
return userIDClaim, identity.GetSessionRef(p.identityManager.Name(), audience, userClaim)
}
+31
View File
@@ -0,0 +1,31 @@
/*
* Copyright 2017-2019 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 (
"crypto"
"github.com/golang-jwt/jwt/v5"
)
// A SigningKey bundles a signer with meta data and a signign method.
type SigningKey struct {
ID string
PrivateKey crypto.Signer
SigningMethod jwt.SigningMethod
}
+105
View File
@@ -0,0 +1,105 @@
/*
* Copyright 2017-2019 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 (
"crypto/sha256"
"encoding/base64"
"encoding/hex"
"fmt"
"net/http"
"net/url"
"github.com/longsleep/rndm"
"golang.org/x/crypto/blake2b"
"github.com/libregraph/lico/identity"
"github.com/libregraph/lico/oidc/payload"
)
var browserStateMarker = []byte("kopano-konnect-1")
func (p *Provider) makeBrowserState(ar *payload.AuthenticationRequest, auth identity.AuthRecord, err error) (string, error) {
hasher, hasherErr := blake2b.New256(nil)
if hasherErr != nil {
return "", hasherErr
}
if auth != nil && err == nil {
hasher.Write([]byte(auth.Subject()))
} else {
// Use empty string value when not signed in or with error. This means
// that a browser state is always created.
hasher.Write([]byte(" "))
}
hasher.Write([]byte(" "))
hasher.Write([]byte(p.issuerIdentifier))
hasher.Write([]byte(" "))
hasher.Write(browserStateMarker)
// Encode to string.
browserState := base64.RawURLEncoding.EncodeToString(hasher.Sum(nil))
return browserState, nil
}
func (p *Provider) makeSessionState(req *http.Request, ar *payload.AuthenticationRequest, browserState string) (string, error) {
var origin string
for {
redirectURL := ar.RedirectURI
if redirectURL != nil {
origin = fmt.Sprintf("%s://%s", redirectURL.Scheme, redirectURL.Host)
break
}
originHeader := req.Header.Get("Origin")
if originHeader != "" {
origin = originHeader
break
}
refererHeader := req.Header.Get("Referer")
if refererHeader != "" {
// Rescure from referer.
refererURL, err := url.Parse(refererHeader)
if err != nil {
return "", fmt.Errorf("invalid referer value: %v", err)
}
origin = fmt.Sprintf("%s://%s", refererURL.Scheme, refererURL.Host)
break
}
return "", fmt.Errorf("missing origin")
}
salt := rndm.GenerateRandomString(32)
hasher := sha256.New()
hasher.Write([]byte(ar.ClientID))
hasher.Write([]byte(" "))
hasher.Write([]byte(origin))
hasher.Write([]byte(" "))
hasher.Write([]byte(browserState))
hasher.Write([]byte(" "))
hasher.Write([]byte(salt))
sessionState := fmt.Sprintf("%s.%s", hex.EncodeToString(hasher.Sum(nil)), salt)
return sessionState, nil
}
+43
View File
@@ -0,0 +1,43 @@
/*
* Copyright 2017-2019 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 (
"errors"
konnect "github.com/libregraph/lico"
"github.com/libregraph/lico/identity"
)
// PublicSubjectFromAuth creates the provideds auth Subject value with the
// accociated provider. This subject can be used as URL safe value to uniquely
// identify the provided auth user with remote systems.
func (p *Provider) PublicSubjectFromAuth(auth identity.AuthRecord) (string, error) {
authorizedScopes := auth.AuthorizedScopes()
if ok, _ := authorizedScopes[konnect.ScopeRawSubject]; ok {
// Return raw subject as is when with ScopeRawSubject.
user := auth.User()
if user == nil {
return "", errors.New("no user")
}
return user.Raw(), nil
}
return auth.Subject(), nil
}
+395
View File
@@ -0,0 +1,395 @@
/*
* 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"
"time"
"github.com/golang-jwt/jwt/v5"
"github.com/libregraph/oidc-go"
"github.com/longsleep/rndm"
konnect "github.com/libregraph/lico"
"github.com/libregraph/lico/identity"
konnectoidc "github.com/libregraph/lico/oidc"
"github.com/libregraph/lico/oidc/payload"
"github.com/libregraph/lico/utils"
)
// MakeAccessToken implements the oidc.AccessTokenProvider interface.
func (p *Provider) MakeAccessToken(ctx context.Context, audience string, auth identity.AuthRecord) (string, error) {
return p.makeAccessToken(ctx, audience, auth, nil, nil)
}
func (p *Provider) makeAccessToken(ctx context.Context, audience string, auth identity.AuthRecord, signingMethod jwt.SigningMethod, refreshTokenClaims *konnect.RefreshTokenClaims) (string, error) {
sk, ok := p.getSigningKey(signingMethod)
if !ok {
return "", fmt.Errorf("no signing key")
}
authorizedScopes := auth.AuthorizedScopes()
authorizedScopesList := payload.ScopesValue(makeArrayFromBoolMap(authorizedScopes))
accessTokenClaims := konnect.AccessTokenClaims{
TokenType: konnect.TokenTypeAccessToken,
AuthorizedScopesList: authorizedScopesList,
AuthorizedClaimsRequest: auth.AuthorizedClaims(),
RegisteredClaims: jwt.RegisteredClaims{
Issuer: p.issuerIdentifier,
Subject: auth.Subject(),
Audience: jwt.ClaimStrings{audience},
ExpiresAt: jwt.NewNumericDate(time.Now().Add(p.accessTokenDuration)),
IssuedAt: jwt.NewNumericDate(time.Now()),
ID: rndm.GenerateRandomString(24),
},
}
user := auth.User()
if user != nil {
if userWithClaims, ok := user.(identity.UserWithClaims); ok {
accessTokenClaims.IdentityClaims = userWithClaims.Claims()
}
accessTokenClaims.IdentityProvider = auth.Manager().Name()
if accessTokenClaims.IdentityClaims != nil && refreshTokenClaims != nil && refreshTokenClaims.IdentityClaims != nil {
if refreshTokenClaims.IdentityProvider != accessTokenClaims.IdentityProvider {
return "", fmt.Errorf("refresh token claims provider mismatch")
}
for k, v := range refreshTokenClaims.IdentityClaims {
// Force to use refresh token identity claim values. This also locks all
// the extra claims for id and access tokens to the ones provided from
// the refresh token claims (which currently includes the session id).
accessTokenClaims.IdentityClaims[k] = v
}
}
}
// Support additional custom user specific claims.
var finalAccessTokenClaims jwt.Claims = accessTokenClaims
if accessTokenClaims.IdentityClaims != nil {
accessTokenClaimsMap, err := payload.ToMap(accessTokenClaims)
if err != nil {
return "", err
}
delete(accessTokenClaimsMap[konnect.IdentityClaim].(map[string]interface{}), konnect.InternalExtraIDTokenClaimsClaim)
delete(accessTokenClaimsMap[konnect.IdentityClaim].(map[string]interface{}), konnect.InternalExtraAccessTokenClaimsClaim)
// Look for special internal key, if its a map all claims in there are
// elevated to top level.
extraClaimsMap, _ := accessTokenClaims.IdentityClaims[konnect.InternalExtraAccessTokenClaimsClaim].(map[string]interface{})
if extraClaimsMap != nil {
// Inject extra claims.
for claim, value := range extraClaimsMap {
switch claim {
case konnect.ScopesClaim:
// Support to extend the scopes.
extraScopesList, _ := value.(string)
if extraScopesList != "" {
authorizedScopesList = append(authorizedScopesList, extraScopesList)
value = authorizedScopesList
}
default:
if _, ok := accessTokenClaimsMap[claim]; ok {
// Prevent override of existing claims, only allow new claims.
continue
}
}
accessTokenClaimsMap[claim] = value
}
}
finalAccessTokenClaims = jwt.MapClaims(accessTokenClaimsMap)
}
accessToken := jwt.NewWithClaims(sk.SigningMethod, finalAccessTokenClaims)
accessToken.Header[oidc.JWTHeaderKeyID] = sk.ID
return accessToken.SignedString(sk.PrivateKey)
}
func (p *Provider) makeIDToken(ctx context.Context, ar *payload.AuthenticationRequest, auth identity.AuthRecord, session *payload.Session, accessTokenString string, codeString string, signingMethod jwt.SigningMethod, refreshTokenClaims *konnect.RefreshTokenClaims) (string, error) {
sk, ok := p.getSigningKey(signingMethod)
if !ok {
return "", fmt.Errorf("no signing key")
}
publicSubject, err := p.PublicSubjectFromAuth(auth)
if err != nil {
return "", err
}
idTokenClaims := &konnectoidc.IDTokenClaims{
Nonce: ar.Nonce,
RegisteredClaims: jwt.RegisteredClaims{
Issuer: p.issuerIdentifier,
Subject: publicSubject,
Audience: jwt.ClaimStrings{ar.ClientID},
ExpiresAt: jwt.NewNumericDate(time.Now().Add(p.idTokenDuration)),
IssuedAt: jwt.NewNumericDate(time.Now()),
},
}
accessTokenClaims := konnect.AccessTokenClaims{}
if session != nil {
// Include session data in ID token.
idTokenClaims.SessionClaims = &konnectoidc.SessionClaims{
SessionID: session.ID,
}
}
// Include requested scope data in ID token when no access token is
// generated.
authorizedClaimsRequest := auth.AuthorizedClaims()
withAccessToken := accessTokenString != ""
withCode := codeString != ""
withAuthTime := ar.MaxAge > 0
withIDTokenClaimsRequest := authorizedClaimsRequest != nil && authorizedClaimsRequest.IDToken != nil
user := auth.User()
if user == nil {
return "", fmt.Errorf("no user")
}
if userWithClaims, ok := user.(identity.UserWithClaims); ok {
accessTokenClaims.IdentityClaims = userWithClaims.Claims()
}
accessTokenClaims.IdentityProvider = auth.Manager().Name()
if accessTokenClaims.IdentityClaims != nil && refreshTokenClaims != nil && refreshTokenClaims.IdentityClaims != nil {
if refreshTokenClaims.IdentityProvider != accessTokenClaims.IdentityProvider {
return "", fmt.Errorf("refresh token claims provider mismatch")
}
for k, v := range refreshTokenClaims.IdentityClaims {
// Force to use refresh token identity claim values. This also locks all
// the extra claims for id and access tokens to the ones provided from
// the refresh token claims (which currently includes the session id).
accessTokenClaims.IdentityClaims[k] = v
}
}
if withIDTokenClaimsRequest {
// Apply additional information from ID token claims request.
if _, ok := authorizedClaimsRequest.IDToken.Get(oidc.AuthTimeClaim); !withAuthTime && ok {
// Return auth time claim if requested and not already requested by other means.
withAuthTime = true
}
}
if !withAccessToken || withIDTokenClaimsRequest {
var userID string
if accessTokenClaims.IdentityClaims != nil {
if userIDString, ok := accessTokenClaims.IdentityClaims[konnect.IdentifiedUserIDClaim]; ok {
userID = userIDString.(string)
}
}
if userID == "" {
return "", fmt.Errorf("no id claim in user identity claims")
}
var sessionRef *string
if userWithSessionRef, ok := user.(identity.UserWithSessionRef); ok {
sessionRef = userWithSessionRef.SessionRef()
}
var requestedClaimsMap []*payload.ClaimsRequestMap
var requestedScopesMap map[string]bool
if withIDTokenClaimsRequest {
requestedClaimsMap = []*payload.ClaimsRequestMap{authorizedClaimsRequest.IDToken}
requestedScopesMap = authorizedClaimsRequest.IDToken.ScopesMap(nil)
}
authorizedScopes := auth.AuthorizedScopes()
freshAuth, found, fetchErr := auth.Manager().Fetch(ctx, userID, sessionRef, authorizedScopes, requestedClaimsMap, authorizedScopes)
if fetchErr != nil {
p.logger.WithFields(utils.ErrorAsFields(fetchErr)).Errorln("identity manager fetch failed")
found = false
}
if !found {
return "", fmt.Errorf("user not found")
}
if (!withAccessToken && ar.Scopes[oidc.ScopeProfile]) || requestedScopesMap[oidc.ScopeProfile] {
idTokenClaims.ProfileClaims = konnectoidc.NewProfileClaims(freshAuth.Claims(oidc.ScopeProfile)[0])
}
if (!withAccessToken && ar.Scopes[oidc.ScopeEmail]) || requestedScopesMap[oidc.ScopeEmail] {
idTokenClaims.EmailClaims = konnectoidc.NewEmailClaims(freshAuth.Claims(oidc.ScopeEmail)[0])
}
auth = freshAuth
}
if withAccessToken {
// Add left-most hash of access token.
// http://openid.net/specs/openid-connect-core-1_0.html#ImplicitIDToken
hash, hashErr := oidc.HashFromSigningMethod(sk.SigningMethod.Alg())
if hashErr != nil {
return "", hashErr
}
idTokenClaims.AccessTokenHash = oidc.LeftmostHash([]byte(accessTokenString), hash).String()
}
if withCode {
// Add left-most hash of code.
// http://openid.net/specs/openid-connect-core-1_0.html#HybridIDToken
hash, hashErr := oidc.HashFromSigningMethod(sk.SigningMethod.Alg())
if hashErr != nil {
return "", hashErr
}
idTokenClaims.CodeHash = oidc.LeftmostHash([]byte(codeString), hash).String()
}
if withAuthTime {
// Add AuthTime.
if loggedOn, logonAt := auth.LoggedOn(); loggedOn {
idTokenClaims.AuthTime = logonAt.Unix()
} else {
// NOTE(longsleep): Return current time to be spec compliant.
idTokenClaims.AuthTime = time.Now().Unix()
}
}
// To support extra non-standard claims in ID token, convert claim set to
// map.
idTokenClaimsMap, err := payload.ToMap(idTokenClaims)
if err != nil {
return "", err
}
if accessTokenClaims.IdentityClaims != nil {
// Inject available extra ID token claims.
extraClaimsMap, _ := accessTokenClaims.IdentityClaims[konnect.InternalExtraIDTokenClaimsClaim].(map[string]interface{})
if extraClaimsMap != nil {
for claim, value := range extraClaimsMap {
idTokenClaimsMap[claim] = value
}
}
}
if !withAccessToken && accessTokenClaims.IdentityClaims != nil {
// Include requested scope data in ID token when no access token is
// generated - additional custom user specific claims.
// Inject extra claims.
extraClaimsMap, _ := accessTokenClaims.IdentityClaims[konnect.InternalExtraAccessTokenClaimsClaim].(map[string]interface{})
if extraClaimsMap != nil {
for claim, value := range extraClaimsMap {
idTokenClaimsMap[claim] = value
}
}
}
// Create signed token.
idToken := jwt.NewWithClaims(sk.SigningMethod, jwt.MapClaims(idTokenClaimsMap))
idToken.Header[oidc.JWTHeaderKeyID] = sk.ID
return idToken.SignedString(sk.PrivateKey)
}
func (p *Provider) makeRefreshToken(ctx context.Context, audience string, auth identity.AuthRecord, signingMethod jwt.SigningMethod) (string, error) {
sk, ok := p.getSigningKey(signingMethod)
if !ok {
return "", fmt.Errorf("no signing key")
}
approvedScopesList := []string{}
approvedScopes := make(map[string]bool)
for scope, granted := range auth.AuthorizedScopes() {
if granted {
approvedScopesList = append(approvedScopesList, scope)
approvedScopes[scope] = true
}
}
ref, err := auth.Manager().ApproveScopes(ctx, auth.Subject(), audience, approvedScopes)
if err != nil {
return "", err
}
refreshTokenClaims := &konnect.RefreshTokenClaims{
TokenType: konnect.TokenTypeRefreshToken,
ApprovedScopesList: approvedScopesList,
ApprovedClaimsRequest: auth.AuthorizedClaims(),
Ref: ref,
RegisteredClaims: jwt.RegisteredClaims{
Issuer: p.issuerIdentifier,
Subject: auth.Subject(),
Audience: jwt.ClaimStrings{audience},
ExpiresAt: jwt.NewNumericDate(time.Now().Add(p.refreshTokenDuration)),
IssuedAt: jwt.NewNumericDate(time.Now()),
ID: rndm.GenerateRandomString(24),
},
}
user := auth.User()
if user != nil {
if userWithClaims, ok := user.(identity.UserWithClaims); ok {
refreshTokenClaims.IdentityClaims = userWithClaims.Claims()
}
refreshTokenClaims.IdentityProvider = auth.Manager().Name()
}
refreshToken := jwt.NewWithClaims(sk.SigningMethod, refreshTokenClaims)
refreshToken.Header[oidc.JWTHeaderKeyID] = sk.ID
return refreshToken.SignedString(sk.PrivateKey)
}
func (p *Provider) makeJWT(ctx context.Context, signingMethod jwt.SigningMethod, claims jwt.Claims) (string, error) {
sk, ok := p.getSigningKey(signingMethod)
if !ok {
return "", fmt.Errorf("no signing key")
}
token := jwt.NewWithClaims(sk.SigningMethod, claims)
token.Header[oidc.JWTHeaderKeyID] = sk.ID
return token.SignedString(sk.PrivateKey)
}
func (p *Provider) validateJWT(token *jwt.Token) (interface{}, error) {
rawAlg, ok := token.Header[oidc.JWTHeaderAlg]
if !ok {
return nil, fmt.Errorf("No alg header")
}
alg, ok := rawAlg.(string)
if !ok {
return nil, fmt.Errorf("Invalid alg value")
}
switch jwt.GetSigningMethod(alg).(type) {
case *jwt.SigningMethodRSA:
case *jwt.SigningMethodECDSA:
case *jwt.SigningMethodRSAPSS:
default:
return nil, fmt.Errorf("Unexpected alg value")
}
rawKid, ok := token.Header[oidc.JWTHeaderKeyID]
if !ok {
return nil, fmt.Errorf("No kid header")
}
kid, ok := rawKid.(string)
if !ok {
return nil, fmt.Errorf("Invalid kid value")
}
key, ok := p.getValidationKey(kid)
if !ok {
return nil, fmt.Errorf("Unknown kid")
}
return key, nil
}
+85
View File
@@ -0,0 +1,85 @@
/*
* Copyright 2017-2019 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 (
"fmt"
"net/http"
"net/url"
"time"
)
var (
farPastExpiryTime = time.Unix(0, 0)
)
func uniqueStrings(s []string) []string {
var res []string
m := make(map[string]bool)
for _, s := range s {
if _, ok := m[s]; ok {
continue
}
m[s] = true
res = append(res, s)
}
return res
}
func getRequestURL(req *http.Request, isTrustedSource bool) *url.URL {
u, _ := url.Parse(req.URL.String())
if isTrustedSource {
// Look at proxy injected values to rewrite URLs if trusted.
for {
prefix := req.Header.Get("X-Forwarded-Prefix")
if prefix != "" {
u.Path = fmt.Sprintf("%s%s", prefix, u.Path)
break
}
replaced := req.Header.Get("X-Replaced-Path")
if replaced != "" {
u.Path = replaced
break
}
break
}
}
return u
}
func addResponseHeaders(header http.Header) {
header.Set("Cache-Control", "no-cache, no-store, must-revalidate")
header.Set("Pragma", "no-cache")
header.Set("X-Content-Type-Options", "nosniff")
header.Set("Referrer-Policy", "origin")
}
func makeArrayFromBoolMap(m map[string]bool) []string {
result := []string{}
for k, v := range m {
if v {
result = append(result, k)
}
}
return result
}