/*
 * 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"
	jose "gopkg.in/square/go-jose.v2"
	jwt "gopkg.in/square/go-jose.v2/jwt"

	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"
	"github.com/libregraph/oidc-go"
)

// 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
	logonCookieName string
	scopesConf      string
	webappIndexHTML []byte

	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,
		logonCookieName: c.LogonCookieName,
		scopesConf:      c.ScopesConf,
		webappIndexHTML: webappIndexHTML,

		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
}
