Files
opencloud/vendor/github.com/libregraph/lico/oidc/provider/provider.go
Christian Richter 16307e036d add new property IdentifierDefaultLogoTargetURI
Signed-off-by: Christian Richter <c.richter@opencloud.eu>
2025-04-28 13:36:13 +02:00

534 lines
17 KiB
Go

/*
* 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
}