mirror of
https://github.com/opencloud-eu/opencloud.git
synced 2026-03-21 17:01:57 -04:00
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.
775 lines
23 KiB
Go
775 lines
23 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 identifier
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"fmt"
|
|
"io/ioutil"
|
|
"net/http"
|
|
"net/url"
|
|
"os"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/deckarep/golang-set"
|
|
"github.com/gorilla/mux"
|
|
"github.com/longsleep/rndm"
|
|
"github.com/sirupsen/logrus"
|
|
"gopkg.in/square/go-jose.v2"
|
|
"gopkg.in/square/go-jose.v2/jwt"
|
|
|
|
"github.com/libregraph/oidc-go"
|
|
|
|
konnect "github.com/libregraph/lico"
|
|
"github.com/libregraph/lico/identifier/backends"
|
|
"github.com/libregraph/lico/identifier/meta"
|
|
"github.com/libregraph/lico/identifier/meta/scopes"
|
|
"github.com/libregraph/lico/identity"
|
|
"github.com/libregraph/lico/identity/authorities"
|
|
"github.com/libregraph/lico/identity/clients"
|
|
"github.com/libregraph/lico/managers"
|
|
"github.com/libregraph/lico/utils"
|
|
)
|
|
|
|
// audienceMarker defines the value which gets included in logon cookies. Valid
|
|
// logon cookies must have the first value of this list in their audience claim.
|
|
// Increment this value whenever logon cookie claims and format changes in
|
|
// non-backwards compatible ways. User will have to sign in again to get a new
|
|
// cookie.
|
|
var audienceMarker = jwt.Audience([]string{"2019012201"})
|
|
|
|
// Identifier defines a identification login area with its endpoints using
|
|
// a Kopano Core server as backend logon provider.
|
|
type Identifier struct {
|
|
Config *Config
|
|
|
|
baseURI *url.URL
|
|
pathPrefix string
|
|
staticFolder string
|
|
scopesConf string
|
|
webappIndexHTML []byte
|
|
|
|
logonCookieName string
|
|
logonCookieSameSite http.SameSite
|
|
|
|
consentCookieSameSite http.SameSite
|
|
|
|
stateCookieSameSite http.SameSite
|
|
|
|
authorizationEndpointURI *url.URL
|
|
signedOutEndpointURI *url.URL
|
|
oauth2CbEndpointURI *url.URL
|
|
|
|
encrypter jose.Encrypter
|
|
recipient *jose.Recipient
|
|
backend backends.Backend
|
|
clients *clients.Registry
|
|
authorities *authorities.Registry
|
|
|
|
meta *meta.Meta
|
|
|
|
defaultBannerLogo *string
|
|
|
|
onSetLogonCallbacks []func(ctx context.Context, rw http.ResponseWriter, user identity.User) error
|
|
onUnsetLogonCallbacks []func(ctx context.Context, rw http.ResponseWriter) error
|
|
|
|
logger logrus.FieldLogger
|
|
|
|
router *mux.Router
|
|
}
|
|
|
|
// NewIdentifier returns a new Identifier.
|
|
func NewIdentifier(c *Config) (*Identifier, error) {
|
|
staticFolder := c.StaticFolder
|
|
var webappIndexHTML = make([]byte, 0)
|
|
|
|
if !c.WebAppDisabled {
|
|
fn := staticFolder + "/index.html"
|
|
if _, statErr := os.Stat(fn); os.IsNotExist(statErr) {
|
|
return nil, fmt.Errorf("identifier client index.html not found: %w", statErr)
|
|
}
|
|
readData, readErr := ioutil.ReadFile(fn)
|
|
if readErr != nil {
|
|
return nil, fmt.Errorf("identifier failed to read client index.html: %w", readErr)
|
|
}
|
|
webappIndexHTML = bytes.Replace(readData, []byte("__PATH_PREFIX__"), []byte(c.PathPrefix), 1)
|
|
}
|
|
|
|
oauth2CbEndpointURI, _ := url.Parse(c.BaseURI.String())
|
|
oauth2CbEndpointURI.Path = c.PathPrefix + "/identifier/oauth2/cb"
|
|
|
|
i := &Identifier{
|
|
Config: c,
|
|
|
|
baseURI: c.BaseURI,
|
|
pathPrefix: c.PathPrefix,
|
|
staticFolder: staticFolder,
|
|
scopesConf: c.ScopesConf,
|
|
webappIndexHTML: webappIndexHTML,
|
|
|
|
logonCookieName: c.LogonCookieName,
|
|
logonCookieSameSite: c.LogonCookieSameSite,
|
|
|
|
consentCookieSameSite: c.ConsentCookieSameSite,
|
|
|
|
stateCookieSameSite: c.StateCookieSameSite,
|
|
|
|
authorizationEndpointURI: c.AuthorizationEndpointURI,
|
|
signedOutEndpointURI: c.SignedOutEndpointURI,
|
|
oauth2CbEndpointURI: oauth2CbEndpointURI,
|
|
|
|
backend: c.Backend,
|
|
|
|
onSetLogonCallbacks: make([]func(ctx context.Context, rw http.ResponseWriter, user identity.User) error, 0),
|
|
onUnsetLogonCallbacks: make([]func(ctx context.Context, rw http.ResponseWriter) error, 0),
|
|
|
|
logger: c.Config.Logger,
|
|
}
|
|
|
|
var err error
|
|
i.meta = &meta.Meta{}
|
|
i.meta.Scopes, err = scopes.NewScopesFromFile(i.scopesConf, i.logger)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if c.DefaultBannerLogo != nil {
|
|
defaultBannerLogo, err := encodeImageAsDataURL(c.DefaultBannerLogo)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to encode default banner logo: %w", err)
|
|
}
|
|
i.defaultBannerLogo = &defaultBannerLogo
|
|
}
|
|
|
|
i.meta.Scopes.Extend(c.Backend.ScopesMeta())
|
|
|
|
return i, nil
|
|
}
|
|
|
|
// RegisterManagers registers the provided managers,
|
|
func (i *Identifier) RegisterManagers(mgrs *managers.Managers) error {
|
|
i.clients = mgrs.Must("clients").(*clients.Registry)
|
|
i.authorities = mgrs.Must("authorities").(*authorities.Registry)
|
|
|
|
if service, ok := i.backend.(managers.ServiceUsesManagers); ok {
|
|
err := service.RegisterManagers(mgrs)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// AddRoutes adds the endpoint routes of the accociated Identifier to the
|
|
// provided router with the provided context.
|
|
func (i *Identifier) AddRoutes(ctx context.Context, router *mux.Router) {
|
|
r := router.PathPrefix(i.pathPrefix).Subrouter()
|
|
|
|
r.PathPrefix("/static/").Handler(i.staticHandler(http.StripPrefix(i.pathPrefix, http.FileServer(http.Dir(i.staticFolder))), true))
|
|
r.Handle("/service-worker.js", i.staticHandler(http.StripPrefix(i.pathPrefix, http.FileServer(http.Dir(i.staticFolder))), false))
|
|
r.Handle("/identifier", http.HandlerFunc(i.handleIdentifier)).Methods(http.MethodGet).Name("index")
|
|
r.Handle("/chooseaccount", i).Methods(http.MethodGet).Name("chooseaccount")
|
|
r.Handle("/consent", i).Methods(http.MethodGet).Name("consent")
|
|
r.Handle("/welcome", i).Methods(http.MethodGet).Name("welcome")
|
|
r.Handle("/goodbye", i).Methods(http.MethodGet).Name("goodbye")
|
|
r.Handle("/index.html", i).Methods(http.MethodGet) // For service worker.
|
|
r.Handle("/identifier/_/logon", i.secureHandler(http.HandlerFunc(i.handleLogon))).Methods(http.MethodPost)
|
|
r.Handle("/identifier/_/logoff", i.secureHandler(http.HandlerFunc(i.handleLogoff))).Methods(http.MethodPost)
|
|
r.Handle("/identifier/_/hello", i.secureHandler(http.HandlerFunc(i.handleHello))).Methods(http.MethodPost)
|
|
r.Handle("/identifier/_/consent", i.secureHandler(http.HandlerFunc(i.handleConsent))).Methods(http.MethodPost)
|
|
r.Handle("/identifier/oauth2/start", http.HandlerFunc(i.handleOAuth2Start)).Methods(http.MethodGet).Name("oauth2/start")
|
|
r.Handle("/identifier/oauth2/cb", http.HandlerFunc(i.handleOAuth2Cb)).Methods(http.MethodGet).Name("oauth2/cb")
|
|
r.Handle("/identifier/saml2/metadata", http.HandlerFunc(i.handleSAML2Metadata))
|
|
r.Handle("/identifier/saml2/acs", http.HandlerFunc(i.handleSAML2AssertionConsumerService)).Methods(http.MethodPost).Name("saml2/acs")
|
|
r.Handle("/identifier/_/saml2/slo", http.HandlerFunc(i.handleSAML2SingleLogoutService)).Methods(http.MethodGet).Name("saml2/slo")
|
|
r.Handle("/identifier/trampolin", http.HandlerFunc(i.handleTrampolin)).Methods(http.MethodGet).Name("trampolin")
|
|
r.Handle("/identifier/trampolin/trampolin.js", http.HandlerFunc(i.handleTrampolin)).Methods(http.MethodGet)
|
|
|
|
i.router = r
|
|
|
|
if i.backend != nil {
|
|
i.backend.RunWithContext(ctx)
|
|
}
|
|
}
|
|
|
|
// ServeHTTP implements the http.Handler interface.
|
|
func (i *Identifier) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
|
|
addCommonResponseHeaders(rw.Header())
|
|
addNoCacheResponseHeaders(rw.Header())
|
|
|
|
// Show default.
|
|
i.writeWebappIndexHTML(rw, req)
|
|
}
|
|
|
|
// SetKey sets the provided key for the accociated identifier.
|
|
func (i *Identifier) SetKey(key []byte) error {
|
|
var ce jose.ContentEncryption
|
|
var algo jose.KeyAlgorithm
|
|
switch len(key) {
|
|
case 16:
|
|
ce = jose.A128GCM
|
|
algo = jose.A128GCMKW
|
|
case 24:
|
|
ce = jose.A192GCM
|
|
algo = jose.A192GCMKW
|
|
case 32:
|
|
ce = jose.A256GCM
|
|
algo = jose.A256GCMKW
|
|
default:
|
|
return fmt.Errorf("identifier invalid encryption key size. Need 16, 24 or 32 bytes")
|
|
}
|
|
|
|
if len(key) < 32 {
|
|
i.logger.Warnf("identifier using encryption key size with %d bytes which is below 32 bytes", len(key))
|
|
} else {
|
|
i.logger.WithField("security", fmt.Sprintf("%s:%s", ce, algo)).Infoln("identifier set up")
|
|
}
|
|
|
|
recipient := jose.Recipient{
|
|
Algorithm: algo,
|
|
KeyID: "",
|
|
Key: key,
|
|
}
|
|
encrypter, err := jose.NewEncrypter(
|
|
ce,
|
|
recipient,
|
|
nil,
|
|
)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
i.encrypter = encrypter
|
|
i.recipient = &recipient
|
|
return nil
|
|
}
|
|
|
|
// ErrorPage writes a HTML error page to the provided ResponseWriter.
|
|
func (i *Identifier) ErrorPage(rw http.ResponseWriter, code int, title string, message string) {
|
|
utils.WriteErrorPage(rw, code, title, message)
|
|
}
|
|
|
|
// SetUserToLogonCookie serializes the provided user into an encrypted string
|
|
// and sets it as cookie on the provided http.ResponseWriter.
|
|
func (i *Identifier) SetUserToLogonCookie(ctx context.Context, rw http.ResponseWriter, user *IdentifiedUser) error {
|
|
loggedOn, logonAt := user.LoggedOn()
|
|
if !loggedOn {
|
|
return fmt.Errorf("refused to set cookie for not logged on user")
|
|
}
|
|
|
|
// Add standard claims.
|
|
claims := jwt.Claims{
|
|
Issuer: user.BackendName(),
|
|
Audience: audienceMarker,
|
|
Subject: user.Subject(),
|
|
IssuedAt: jwt.NewNumericDate(logonAt),
|
|
}
|
|
// Add expiration, if set.
|
|
if user.expiresAfter != nil {
|
|
claims.Expiry = jwt.NewNumericDate(*user.expiresAfter)
|
|
}
|
|
|
|
// Additional claims.
|
|
userClaims := map[string]interface{}(user.Claims())
|
|
if sessionRef := user.SessionRef(); sessionRef != nil {
|
|
userClaims[SessionIDClaim] = *sessionRef
|
|
}
|
|
if logonRef := user.LogonRef(); logonRef != nil {
|
|
userClaims[LogonRefClaim] = *logonRef
|
|
}
|
|
if externalAuthorityID := user.ExternalAuthorityID(); externalAuthorityID != nil {
|
|
userClaims[ExternalAuthorityIDClaim] = *externalAuthorityID
|
|
}
|
|
if lockedScopes := user.LockedScopes(); lockedScopes != nil {
|
|
userClaims[LockedScopesClaim] = strings.Join(lockedScopes, " ")
|
|
}
|
|
|
|
// Serialize and encrypt cookie value.
|
|
serialized, err := jwt.Encrypted(i.encrypter).Claims(claims).Claims(userClaims).CompactSerialize()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Set cookie.
|
|
err = i.setLogonCookie(rw, serialized)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
// Trigger callbacks.
|
|
for _, f := range i.onSetLogonCallbacks {
|
|
err = f(ctx, rw, user)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// UnsetLogonCookie adds cookie remove headers to the provided http.ResponseWriter
|
|
// effectively implementing logout.
|
|
func (i *Identifier) UnsetLogonCookie(ctx context.Context, user *IdentifiedUser, rw http.ResponseWriter) error {
|
|
// Remove cookie.
|
|
err := i.removeLogonCookie(rw)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
// Destroy backend user session if any.
|
|
if user != nil {
|
|
if sessionRef := user.SessionRef(); sessionRef != nil {
|
|
err = i.backend.DestroySession(ctx, sessionRef)
|
|
if err != nil {
|
|
i.logger.WithError(err).Warnln("failed to destroy session on unset logon cookie")
|
|
}
|
|
}
|
|
}
|
|
// Trigger callbacks.
|
|
for _, f := range i.onUnsetLogonCallbacks {
|
|
err = f(ctx, rw)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// EndSession begins the process to end the session either directly or indirectly
|
|
// based on the provided user. It optionally returns an uri which shall be used
|
|
// as redirection target or an error.
|
|
func (i *Identifier) EndSession(ctx context.Context, user *IdentifiedUser, rw http.ResponseWriter, postRedirectURI *url.URL, state string) (*url.URL, error) {
|
|
err := i.UnsetLogonCookie(ctx, user, rw)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var uri *url.URL
|
|
if user.externalAuthority != nil && user.externalAuthority.EndSessionEnabled {
|
|
// Generate state and set state cookie with postRedirectURI.
|
|
if state == "" {
|
|
state = rndm.GenerateRandomString(32)
|
|
}
|
|
sd := &StateData{
|
|
State: state,
|
|
Mode: StateModeEndSession,
|
|
|
|
Ref: user.externalAuthority.ID,
|
|
}
|
|
var extra map[string]interface{}
|
|
uri, extra, err = user.externalAuthority.MakeRedirectEndSessionRequestURL(user.LogonRef(), sd.State)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
sd.Extra = extra
|
|
if postRedirectURI != nil && postRedirectURI.String() != "" {
|
|
sd.RawQuery = postRedirectURI.String()
|
|
}
|
|
|
|
var scope string
|
|
switch user.externalAuthority.AuthorityType {
|
|
case authorities.AuthorityTypeOIDC:
|
|
// Inject post logout url target.
|
|
cb, _ := i.router.GetRoute("oauth2/cb").URL()
|
|
next, _ := url.Parse(i.baseURI.String())
|
|
next.Path = cb.Path
|
|
query := uri.Query()
|
|
query.Set("post_logout_redirect_uri", next.String())
|
|
uri.RawQuery = query.Encode()
|
|
// Redirect using trampolin, to ensure origin checks of external
|
|
// authority can pass.
|
|
sd.Trampolin = &TrampolinData{
|
|
URI: uri.String(),
|
|
Scope: "oauth2/cb",
|
|
}
|
|
scope = "trampolin"
|
|
uri, _ = i.router.GetRoute("trampolin").URL()
|
|
query = make(url.Values)
|
|
query.Add("state", sd.State)
|
|
uri.RawQuery = query.Encode()
|
|
|
|
case authorities.AuthorityTypeSAML2:
|
|
scope = "_/saml2/slo"
|
|
}
|
|
|
|
if scope != "" {
|
|
err = i.SetStateToStateCookie(ctx, rw, scope, sd)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to set saml2 slo state cookie: %w", err)
|
|
}
|
|
}
|
|
}
|
|
|
|
return uri, nil
|
|
}
|
|
|
|
// GetUserFromLogonCookie looks up the associated cookie name from the provided
|
|
// request, parses it and returns the user containing the information found in
|
|
// the coookie payload data.
|
|
func (i *Identifier) GetUserFromLogonCookie(ctx context.Context, req *http.Request, maxAge time.Duration, refreshSession bool) (*IdentifiedUser, error) {
|
|
cookie, err := i.getLogonCookie(req)
|
|
if err != nil {
|
|
if err == http.ErrNoCookie {
|
|
return nil, nil
|
|
}
|
|
return nil, err
|
|
}
|
|
|
|
// Decrypt and parse cookie value.
|
|
token, err := jwt.ParseEncrypted(cookie.Value)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Parse claims.
|
|
var claims jwt.Claims
|
|
var userClaims map[string]interface{}
|
|
if claimsErr := token.Claims(i.recipient.Key, &claims, &userClaims); claimsErr != nil {
|
|
return nil, claimsErr
|
|
}
|
|
|
|
// Validate claims.
|
|
if claimsErr := claims.Validate(jwt.Expected{
|
|
// Ignore cookie, when issuer does not match our backend name. This usually
|
|
// means that konnect was reconfigured. Users need to sign in again.
|
|
Issuer: i.backend.Name(),
|
|
// Ignore cookie, when audience marker does not match. This happens
|
|
// for cookies from an older version of konnect. Users need to sign in again.
|
|
Audience: jwt.Audience{audienceMarker[0]},
|
|
}); claimsErr != nil {
|
|
i.logger.WithError(claimsErr).Debugln("logon token claims validation failed")
|
|
return nil, nil
|
|
}
|
|
if claims.Subject == "" {
|
|
return nil, fmt.Errorf("invalid subject in logon token")
|
|
}
|
|
if userClaims == nil {
|
|
return nil, fmt.Errorf("invalid user claims in logon token")
|
|
}
|
|
|
|
// New user with details from claims.
|
|
user := &IdentifiedUser{
|
|
sub: claims.Subject,
|
|
|
|
// TODO(longsleep): It is not verified here that the user still exists at
|
|
// our current backend. We still assign the backend happily here - probably
|
|
// needs some sort of veritification / lookup.
|
|
backend: i.backend,
|
|
|
|
logonAt: claims.IssuedAt.Time(),
|
|
}
|
|
if claims.Expiry != nil {
|
|
expiresAfter := claims.Expiry.Time()
|
|
user.expiresAfter = &expiresAfter
|
|
}
|
|
|
|
loggedOn, logonAt := user.LoggedOn()
|
|
if !loggedOn {
|
|
// Ignore logons which are not valid.
|
|
return nil, nil
|
|
}
|
|
if maxAge > 0 {
|
|
if logonAt.Add(maxAge).Before(time.Now()) {
|
|
// Ignore logon as it is no longer valid within maxAge.
|
|
return nil, nil
|
|
}
|
|
}
|
|
|
|
// Get specific data from claims.
|
|
if v := userClaims[SessionIDClaim]; v != nil {
|
|
sessionRef := v.(string)
|
|
if sessionRef != "" {
|
|
// Remember session ref in user.
|
|
user.sessionRef = &sessionRef
|
|
// Ensure the session is still valid, by refreshing it.
|
|
if refreshSession {
|
|
err = i.backend.RefreshSession(ctx, user.Subject(), &sessionRef, userClaims)
|
|
if err != nil {
|
|
// Ignore logons which fail session refresh.
|
|
return nil, nil
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if v := userClaims[LogonRefClaim]; v != nil {
|
|
logonRef := v.(string)
|
|
if logonRef != "" {
|
|
// Remember logon ref in user.
|
|
user.logonRef = &logonRef
|
|
}
|
|
}
|
|
if v := userClaims[ExternalAuthorityIDClaim]; v != nil {
|
|
externalAuthorityID := v.(string)
|
|
if externalAuthorityID != "" {
|
|
authority, err := i.authorities.Lookup(ctx, externalAuthorityID)
|
|
if err != nil {
|
|
// Ignore logons which have set an unknown external authority.
|
|
return nil, nil
|
|
}
|
|
// TODO(longsleep): Check if authority is actually enabled. For now
|
|
// we check if it is ready.
|
|
if !authority.IsReady() {
|
|
// Ignore logons which have sent an authority which is not ready.
|
|
return nil, nil
|
|
}
|
|
user.externalAuthority = authority
|
|
}
|
|
}
|
|
|
|
if v := userClaims[LockedScopesClaim]; v != nil {
|
|
lockedScopes := v.(string)
|
|
if lockedScopes != "" {
|
|
user.lockedScopes = strings.Split(lockedScopes, " ")
|
|
}
|
|
}
|
|
|
|
// Fill additional claim.
|
|
user.claims = make(map[string]interface{})
|
|
for k, v := range userClaims {
|
|
switch k {
|
|
case konnect.IdentifiedUsernameClaim:
|
|
user.username = v.(string)
|
|
case konnect.IdentifiedDisplayNameClaim:
|
|
user.displayName = v.(string)
|
|
|
|
case SessionIDClaim:
|
|
// Already handled above.
|
|
continue
|
|
case LogonRefClaim:
|
|
// Already handled above.
|
|
continue
|
|
case ExternalAuthorityIDClaim:
|
|
// Already handled above.
|
|
continue
|
|
case LockedScopesClaim:
|
|
// Already handled above.
|
|
continue
|
|
case ObsoleteUserClaimsClaim:
|
|
// Keep and ignore for history reasons.
|
|
continue
|
|
|
|
case oidc.AudienceClaim, oidc.IssuedAtClaim, oidc.ExpirationClaim, oidc.SubjectIdentifierClaim, oidc.IssuerIdentifierClaim:
|
|
// Ignore default OIDC claims when resurrecting claims data.
|
|
continue
|
|
|
|
default:
|
|
// Add the rest.
|
|
user.claims[k] = v
|
|
}
|
|
}
|
|
|
|
return user, nil
|
|
}
|
|
|
|
// GetUserFromID looks up the user identified by the provided userID by
|
|
// requesting the associated backend.
|
|
func (i *Identifier) GetUserFromID(ctx context.Context, userID string, sessionRef *string, requestedScopes map[string]bool) (*IdentifiedUser, error) {
|
|
user, err := i.backend.GetUser(ctx, userID, sessionRef, requestedScopes)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if user == nil {
|
|
return nil, nil
|
|
}
|
|
|
|
// XXX(longsleep): This is quite crappy. Move IdentifiedUser to a package
|
|
// which can be imported by backends so they directly can return that shit.
|
|
identifiedUser := &IdentifiedUser{
|
|
sub: user.Subject(),
|
|
|
|
username: user.Username(),
|
|
|
|
backend: i.backend,
|
|
|
|
sessionRef: sessionRef,
|
|
claims: user.BackendClaims(),
|
|
scopes: user.BackendScopes(),
|
|
|
|
lockedScopes: user.RequiredScopes(),
|
|
}
|
|
if userWithEmail, ok := user.(identity.UserWithEmail); ok {
|
|
identifiedUser.email = userWithEmail.Email()
|
|
identifiedUser.emailVerified = userWithEmail.EmailVerified()
|
|
}
|
|
if userWithProfile, ok := user.(identity.UserWithProfile); ok {
|
|
identifiedUser.displayName = userWithProfile.Name()
|
|
identifiedUser.familyName = userWithProfile.FamilyName()
|
|
identifiedUser.givenName = userWithProfile.GivenName()
|
|
}
|
|
if userWithID, ok := user.(identity.UserWithID); ok {
|
|
identifiedUser.id = userWithID.ID()
|
|
}
|
|
if userWithUniqueID, ok := user.(identity.UserWithUniqueID); ok {
|
|
identifiedUser.uid = userWithUniqueID.UniqueID()
|
|
}
|
|
|
|
return identifiedUser, nil
|
|
}
|
|
|
|
// SetConsentToConsentCookie serializses the provided Consent using the provided
|
|
// ConsentRequest and sets it as cookie on the provided ReponseWriter.
|
|
func (i *Identifier) SetConsentToConsentCookie(ctx context.Context, rw http.ResponseWriter, cr *ConsentRequest, consent *Consent) error {
|
|
serialized, err := jwt.Encrypted(i.encrypter).Claims(consent).CompactSerialize()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return i.setConsentCookie(rw, cr, serialized)
|
|
}
|
|
|
|
// GetConsentFromConsentCookie extract consent information for the provided
|
|
// request and the provide state.
|
|
func (i *Identifier) GetConsentFromConsentCookie(ctx context.Context, rw http.ResponseWriter, req *http.Request, state string) (*Consent, error) {
|
|
if state == "" {
|
|
return nil, nil
|
|
}
|
|
|
|
cr := &ConsentRequest{
|
|
State: state,
|
|
ClientID: req.Form.Get("client_id"),
|
|
RawRedirectURI: req.Form.Get("redirect_uri"),
|
|
Ref: req.Form.Get("state"),
|
|
Nonce: req.Form.Get("nonce"),
|
|
}
|
|
|
|
cookie, err := i.getConsentCookie(req, cr)
|
|
if err != nil {
|
|
if err == http.ErrNoCookie {
|
|
return nil, nil
|
|
}
|
|
return nil, err
|
|
}
|
|
|
|
// Directly remove the cookie again after we used it.
|
|
i.removeConsentCookie(rw, req, cr)
|
|
|
|
token, err := jwt.ParseEncrypted(cookie.Value)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var consent Consent
|
|
if err = token.Claims(i.recipient.Key, &consent); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &consent, nil
|
|
}
|
|
|
|
// SetStateToStateCookie serializses the provided StateRequest and sets it
|
|
// as cookie on the provided ReponseWriter.
|
|
func (i *Identifier) SetStateToStateCookie(ctx context.Context, rw http.ResponseWriter, scope string, sd *StateData) error {
|
|
serialized, err := jwt.Encrypted(i.encrypter).Claims(sd).CompactSerialize()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return i.setStateCookie(rw, scope, sd.State, serialized)
|
|
}
|
|
|
|
// GetStateFromStateCookie extracts state information for the provided
|
|
// request using the provided scope and state.
|
|
func (i *Identifier) GetStateFromStateCookie(ctx context.Context, rw http.ResponseWriter, req *http.Request, scope string, state string) (*StateData, error) {
|
|
if state == "" {
|
|
return nil, nil
|
|
}
|
|
|
|
cookie, err := i.getStateCookie(req, state)
|
|
if err != nil {
|
|
if err == http.ErrNoCookie {
|
|
return nil, nil
|
|
}
|
|
return nil, err
|
|
}
|
|
|
|
// Directly remove the cookie again after we used it.
|
|
i.removeStateCookie(rw, req, scope, state)
|
|
|
|
token, err := jwt.ParseEncrypted(cookie.Value)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
sd := &StateData{}
|
|
if err = token.Claims(i.recipient.Key, sd); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if sd.State != state {
|
|
return nil, fmt.Errorf("state mismatch")
|
|
}
|
|
|
|
return sd, nil
|
|
}
|
|
|
|
// Name returns the active identifiers backend's name.
|
|
func (i *Identifier) Name() string {
|
|
return i.backend.Name()
|
|
}
|
|
|
|
// ScopesSupported return the scopes supported by the accociated Identifier.
|
|
func (i *Identifier) ScopesSupported() []string {
|
|
scopes := mapset.NewThreadUnsafeSet()
|
|
|
|
for scope := range i.meta.Scopes.Definitions {
|
|
scopes.Add(scope)
|
|
}
|
|
for _, scope := range i.backend.ScopesSupported() {
|
|
scopes.Add(scope)
|
|
}
|
|
|
|
supportedScopes := make([]string, 0)
|
|
it := scopes.Iterator()
|
|
for scope := range it.C {
|
|
supportedScopes = append(supportedScopes, scope.(string))
|
|
}
|
|
|
|
return supportedScopes
|
|
}
|
|
|
|
// OnSetLogon implements a way to register hooks whenever logon information is
|
|
// set by the accociated Identifier.
|
|
func (i *Identifier) OnSetLogon(cb func(ctx context.Context, rw http.ResponseWriter, user identity.User) error) error {
|
|
i.onSetLogonCallbacks = append(i.onSetLogonCallbacks, cb)
|
|
return nil
|
|
}
|
|
|
|
// OnUnsetLogon implements a way to register hooks whenever logon information is
|
|
// set by the accociated Identifier.
|
|
func (i *Identifier) OnUnsetLogon(cb func(ctx context.Context, rw http.ResponseWriter) error) error {
|
|
i.onUnsetLogonCallbacks = append(i.onUnsetLogonCallbacks, cb)
|
|
return nil
|
|
}
|
|
|
|
func (i *Identifier) absoluteURLForRoute(name string) (*url.URL, error) {
|
|
uri, _ := url.Parse(i.Config.BaseURI.String())
|
|
|
|
route := i.router.Get(name)
|
|
|
|
path, err := route.URL()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
uri.Path = path.Path
|
|
|
|
return uri, nil
|
|
}
|