To enhance the security of our application and prevent Cross-Site Request Forgery (CSRF) attacks, we have updated the SameSite attribute of the build in Identity Provider (IDP) cookies to Strict.
535 lines
17 KiB
Go
535 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/v4"
|
|
"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/signing"
|
|
"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 = signing.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 *signing.SigningMethodEdwardsCurve:
|
|
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(),
|
|
signing.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
|
|
}
|