mirror of
https://github.com/opencloud-eu/opencloud.git
synced 2026-01-04 12:10:21 -05:00
534 lines
17 KiB
Go
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
|
|
}
|