Files
opencloud/vendor/github.com/libregraph/lico/identity/managers/identifier.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

563 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 managers
import (
"context"
"fmt"
"net/http"
"net/url"
"strings"
"github.com/gorilla/mux"
"github.com/libregraph/oidc-go"
"github.com/longsleep/rndm"
"github.com/sirupsen/logrus"
"github.com/libregraph/lico/identifier"
"github.com/libregraph/lico/identity"
"github.com/libregraph/lico/identity/clients"
"github.com/libregraph/lico/managers"
konnectoidc "github.com/libregraph/lico/oidc"
"github.com/libregraph/lico/oidc/payload"
"github.com/libregraph/lico/utils"
)
// IdentifierIdentityManager implements an identity manager which relies on
// Konnect its identifier to provide identity.
type IdentifierIdentityManager struct {
signInFormURI string
signedOutURI string
scopesSupported []string
claimsSupported []string
identifier *identifier.Identifier
clients *clients.Registry
logger logrus.FieldLogger
}
type identifierUser struct {
*identifier.IdentifiedUser
}
func (u *identifierUser) Raw() string {
return u.IdentifiedUser.Subject()
}
func (u *identifierUser) Subject() string {
sub, _ := getPublicSubject([]byte(u.Raw()), []byte(u.IdentifiedUser.BackendName()))
return sub
}
func (u *identifierUser) Scopes() []string {
return u.IdentifiedUser.Scopes()
}
func (u *identifierUser) RequiredScopes() map[string]bool {
lockedScopes := u.IdentifiedUser.LockedScopes()
if lockedScopes == nil {
return nil
}
requiredScopes := make(map[string]bool)
for _, scope := range lockedScopes {
if strings.HasPrefix(scope, "!") {
scope = strings.TrimLeft(scope, "!")
requiredScopes[scope] = false
} else {
requiredScopes[scope] = true
}
}
return requiredScopes
}
func asIdentifierUser(user *identifier.IdentifiedUser) *identifierUser {
return &identifierUser{user}
}
// NewIdentifierIdentityManager creates a new IdentifierIdentityManager from the provided
// parameters.
func NewIdentifierIdentityManager(c *identity.Config, i *identifier.Identifier) *IdentifierIdentityManager {
im := &IdentifierIdentityManager{
signInFormURI: c.SignInFormURI.String(),
signedOutURI: c.SignedOutURI.String(),
scopesSupported: setupSupportedScopes([]string{
oidc.ScopeOfflineAccess,
}, nil, c.ScopesSupported),
claimsSupported: []string{
oidc.NameClaim,
oidc.FamilyNameClaim,
oidc.GivenNameClaim,
oidc.EmailClaim,
oidc.EmailVerifiedClaim,
},
identifier: i,
logger: c.Logger,
}
return im
}
// RegisterManagers registers the provided managers,
func (im *IdentifierIdentityManager) RegisterManagers(mgrs *managers.Managers) error {
im.clients = mgrs.Must("clients").(*clients.Registry)
return im.identifier.RegisterManagers(mgrs)
}
// Authenticate implements the identity.Manager interface.
func (im *IdentifierIdentityManager) Authenticate(ctx context.Context, rw http.ResponseWriter, req *http.Request, ar *payload.AuthenticationRequest, next identity.Manager) (identity.AuthRecord, error) {
var user *identifierUser
var err error
if authenticationErrorID := req.Form.Get("error"); authenticationErrorID != "" {
// Incoming with error. Directly abort and return.
return nil, ar.NewError(authenticationErrorID, req.Form.Get("error_description"))
}
u, _ := im.identifier.GetUserFromLogonCookie(ctx, req, ar.MaxAge, true)
if u != nil {
// TODO(longsleep): Add other user meta data.
user = asIdentifierUser(u)
} else {
// Not signed in.
if mode := req.Form.Get("identifier"); mode == identifier.MustBeSignedIn {
// Identifier mode is set to must, this means that this flow must be authenticated here, and everything
// else is an error. This is for example set, when coming back from an external authority.
im.logger.WithField("mode", mode).Debugln("identifier mode is set, but not signed in")
} else if next != nil {
// Give next handler a chance if any.
if auth, authErr := next.Authenticate(ctx, rw, req, ar, nil); authErr == nil {
// Inner handler success.
// TODO(longsleep): Add check and option to avoid that the inner
// handler can ever return users which exist at the outer.
return auth, authErr
} else {
switch authErr.(type) {
case *payload.AuthenticationError:
// ignore, breaks
case *identity.LoginRequiredError:
// ignore, breaks
case *identity.IsHandledError:
// breaks, breaks
default:
im.logger.WithFields(utils.ErrorAsFields(authErr)).Errorln("inner authorize request failed")
}
}
}
err = ar.NewError(oidc.ErrorCodeOIDCLoginRequired, "IdentifierIdentityManager: not signed in")
}
// Check prompt value.
switch {
case ar.Prompts[oidc.PromptNone] == true:
if err != nil {
// Never show sign-in, directly return error.
return nil, err
}
case ar.Prompts[oidc.PromptLogin] == true:
if err == nil {
// Enforce to show sign-in, when signed in.
err = ar.NewError(oidc.ErrorCodeOIDCLoginRequired, "IdentifierIdentityManager: prompt=login request")
}
case ar.Prompts[oidc.PromptSelectAccount] == true:
if err == nil {
// Enforce to show sign-in, when signed in.
err = ar.NewError(oidc.ErrorCodeOIDCLoginRequired, "IdentifierIdentityManager: prompt=select_account request")
}
default:
// Let all other prompt values pass.
}
var auth identity.AuthRecord
// More checks.
if err == nil {
var sub string
if user != nil {
sub = user.Subject()
}
err = ar.Verify(sub)
if err != nil {
return nil, err
}
if user != nil {
record := identifier.NewRecord(req, im.identifier.Config.Config)
record.IdentifiedUser = user.IdentifiedUser
ctx = identifier.NewRecordContext(ctx, record)
// Inject required scopes into request.
for scope, ok := range user.RequiredScopes() {
ar.Scopes[scope] = ok
}
// Load user record from identitymanager, without any scopes or claims
// to ensure that the user data is refreshed and that the user still
// exists.
var found bool
auth, found, err = im.Fetch(ctx, user.Raw(), user.SessionRef(), nil, nil, ar.Scopes)
if !found {
err = konnectoidc.NewOAuth2Error(oidc.ErrorCodeOAuth2ServerError, "user not found")
} else {
// Update ar.Scopes with the ones gotten from backend.
if bu, ok := auth.User().(*identifierUser); ok {
scopes := bu.Scopes()
if scopes != nil {
expanded := make(map[string]bool)
for _, scope := range scopes {
if enabled, ok := ar.Scopes[scope]; ok && !enabled {
// Skip already known but not enabled scopes.
continue
}
expanded[scope] = true
}
ar.Scopes = expanded
}
}
}
}
}
if err != nil {
if ar.Prompts[oidc.PromptNone] == true {
// Never show sign-in, directly return error.
return nil, err
}
// Build login URL.
query, err := url.ParseQuery(req.URL.RawQuery)
if err != nil {
return nil, err
}
query.Set("flow", identifier.FlowOIDC)
if ar.Claims != nil {
// Add derived scope list from claims request.
claimsScopes := ar.Claims.Scopes(ar.Scopes)
if len(claimsScopes) > 0 {
query.Set("claims_scope", strings.Join(claimsScopes, " "))
}
}
u, _ := url.Parse(im.signInFormURI)
u.RawQuery = query.Encode()
utils.WriteRedirect(rw, http.StatusFound, u, nil, false)
return nil, &identity.IsHandledError{}
}
if auth == nil {
// In case no existing user was fetched and that was not an error, make
// sure that we actually create a new auth record. This should not
// happen and is kept here for potential backwards compatibility.
auth = identity.NewAuthRecord(im, user.Subject(), nil, nil, nil)
auth.SetUser(user)
}
if loggedOn, logonAt := u.LoggedOn(); loggedOn {
auth.SetAuthTime(logonAt)
}
return auth, nil
}
// Authorize implements the identity.Manager interface.
func (im *IdentifierIdentityManager) Authorize(ctx context.Context, rw http.ResponseWriter, req *http.Request, ar *payload.AuthenticationRequest, auth identity.AuthRecord) (identity.AuthRecord, error) {
promptConsent := false
var approvedScopes map[string]bool
// Check prompt value.
switch {
case ar.Prompts[oidc.PromptConsent] == true:
promptConsent = true
default:
// Let all other prompt values pass.
}
origin := ""
if false {
// TODO(longsleep): find a condition when this can be enabled.
origin = utils.OriginFromRequestHeaders(req.Header)
}
clientDetails, err := im.clients.Lookup(req.Context(), ar.ClientID, "", ar.RedirectURI, origin, true)
if err != nil {
return nil, ar.NewError(oidc.ErrorCodeOAuth2AccessDenied, err.Error())
}
// If not trusted, always force consent.
if clientDetails.Trusted {
approvedScopes = ar.Scopes
} else {
promptConsent = true
}
// Check given consent.
consent, err := im.identifier.GetConsentFromConsentCookie(req.Context(), rw, req, req.Form.Get("konnect"))
if err != nil {
return nil, err
}
if consent != nil {
if !consent.Allow {
return auth, ar.NewError(oidc.ErrorCodeOAuth2AccessDenied, "consent denied")
}
promptConsent = false
filteredApprovedScopes, allApprovedScopes := consent.Scopes(ar.Scopes)
// Filter claims request by approved scopes.
if ar.Claims != nil {
err = ar.Claims.ApplyScopes(allApprovedScopes)
if err != nil {
return nil, err
}
}
approvedScopes = filteredApprovedScopes
}
if promptConsent {
if ar.Prompts[oidc.PromptNone] == true {
return auth, ar.NewError(oidc.ErrorCodeOIDCInteractionRequired, "consent required")
}
// Build consent URL.
query, err := url.ParseQuery(req.URL.RawQuery)
if err != nil {
return nil, err
}
query.Set("flow", identifier.FlowConsent)
if ar.Claims != nil {
// Add derived scope list from claims request.
claimsScopes := ar.Claims.Scopes(ar.Scopes)
if len(claimsScopes) > 0 {
query.Set("claims_scope", strings.Join(claimsScopes, " "))
}
}
if ar.Scopes != nil {
scopes := make([]string, 0)
for scope, ok := range ar.Scopes {
if ok {
scopes = append(scopes, scope)
}
query.Set("scope", strings.Join(scopes, " "))
}
}
u, _ := url.Parse(im.signInFormURI)
u.RawQuery = query.Encode()
utils.WriteRedirect(rw, http.StatusFound, u, nil, false)
return nil, &identity.IsHandledError{}
}
// Offline access validation.
// http://openid.net/specs/openid-connect-core-1_0.html#OfflineAccess
if ok, _ := approvedScopes[oidc.ScopeOfflineAccess]; ok {
var ignoreOfflineAccessErr error
for {
if ok, _ := ar.ResponseTypes[oidc.ResponseTypeCode]; !ok {
// MUST ignore the offline_access request unless the Client is using
// a response_type value that would result in an Authorization
// Code being returned,
ignoreOfflineAccessErr = fmt.Errorf("response_type=code required, %#v", ar.ResponseTypes)
break
}
if clientDetails.Trusted {
// Always allow offline access for trusted clients. This qualifies
// for other conditions.
break
}
if ok, _ := ar.Prompts[oidc.PromptConsent]; !ok && consent == nil {
// Ensure that the prompt parameter contains consent unless
// other conditions for processing the request permitting offline
// access to the requested resources are in place; unless one or
// both of these conditions are fulfilled, then it MUST ignore the
// offline_access request,
ignoreOfflineAccessErr = fmt.Errorf("prompt=consent required, %#v", ar.Prompts)
break
}
break
}
if ignoreOfflineAccessErr != nil {
delete(approvedScopes, oidc.ScopeOfflineAccess)
im.logger.WithError(ignoreOfflineAccessErr).Debugln("removed offline_access scope")
}
}
auth.AuthorizeScopes(approvedScopes)
auth.AuthorizeClaims(ar.Claims)
return auth, nil
}
// EndSession implements the identity.Manager interface.
func (im *IdentifierIdentityManager) EndSession(ctx context.Context, rw http.ResponseWriter, req *http.Request, esr *payload.EndSessionRequest) error {
var err error
var esrClaims *konnectoidc.IDTokenClaims
var clientDetails *clients.Details
origin := utils.OriginFromRequestHeaders(req.Header)
clientID := ""
if esr.IDTokenHint != nil {
// Extended request, verify IDTokenHint and its claims if available.
esrClaims = esr.IDTokenHint.Claims.(*konnectoidc.IDTokenClaims)
if len(esrClaims.Audience) == 1 {
clientID = esrClaims.Audience[0]
}
clientDetails, err = im.clients.Lookup(ctx, clientID, "", esr.PostLogoutRedirectURI, origin, true)
if err != nil {
// This error is not fatal since according to
// the spec in https://openid.net/specs/openid-connect-session-1_0.html#RPLogout the
// id_token_hint is not enforced to match the audience. Instead of fail
// we treat it as untrusted client.
im.logger.WithError(err).Debugln("IdentifierIdentityManager: id_token_hint does not match request")
esrClaims = nil
clientDetails = nil
}
}
var user *identifierUser
u, _ := im.identifier.GetUserFromLogonCookie(ctx, req, 0, false)
if u != nil {
user = asIdentifierUser(u)
// More checks.
if clientDetails != nil && user != nil {
sub := user.Subject()
err = esr.Verify(sub)
if err != nil {
return err
}
}
if clientDetails != nil && clientDetails.Trusted {
// Directly end identifier session when a trusted client requests
// and honor redirect wish if any.
var uri *url.URL
uri, err = im.identifier.EndSession(ctx, u, rw, esr.PostLogoutRedirectURI, esr.State)
if err != nil {
// Do nothing if err.
im.logger.WithError(err).Errorln("IdentifierIdentityManager: failed to end session")
return err
}
if uri != nil {
// Redirect to uri if end session returned any.
return identity.NewRedirectError("", uri)
}
}
} else {
// Ignore when not signed in, for end session.
}
if clientDetails == nil || !clientDetails.Trusted || esr.PostLogoutRedirectURI == nil || esr.PostLogoutRedirectURI.String() == "" {
// Handle directly by redirecting to our logout confirm url for untrusted
// clients or when no URL was set.
u, _ := url.Parse(im.signedOutURI)
query := &url.Values{}
if clientDetails != nil {
query.Add("flow", identifier.FlowOIDC)
}
if esrClaims != nil {
query.Add("client_id", clientID)
}
u.RawQuery = query.Encode()
return identity.NewRedirectError(oidc.ErrorCodeOIDCInteractionRequired, u)
}
return nil
}
// ApproveScopes implements the Backend interface.
func (im *IdentifierIdentityManager) ApproveScopes(ctx context.Context, sub string, audience string, approvedScopes map[string]bool) (string, error) {
ref := rndm.GenerateRandomString(32)
// TODO(longsleep): Store generated ref with provided data.
return ref, nil
}
// ApprovedScopes implements the Backend interface.
func (im *IdentifierIdentityManager) ApprovedScopes(ctx context.Context, sub string, audience string, ref string) (map[string]bool, error) {
if ref == "" {
return nil, fmt.Errorf("IdentifierIdentityManager: invalid ref")
}
return nil, nil
}
// Fetch implements the identity.Manager interface.
func (im *IdentifierIdentityManager) Fetch(ctx context.Context, userID string, sessionRef *string, scopes map[string]bool, requestedClaimsMaps []*payload.ClaimsRequestMap, requestedScopes map[string]bool) (identity.AuthRecord, bool, error) {
u, err := im.identifier.GetUserFromID(ctx, userID, sessionRef, requestedScopes)
if err != nil {
im.logger.WithError(err).Errorln("IdentifierIdentityManager: fetch failed to get user from userID")
return nil, false, fmt.Errorf("IdentifierIdentityManager: identifier error")
}
if u == nil {
return nil, false, fmt.Errorf("IdentifierIdentityManager: no user")
}
user := asIdentifierUser(u)
authorizedScopes, _ := identity.AuthorizeScopes(im, user, scopes)
claims := identity.GetUserClaimsForScopes(user, authorizedScopes, requestedClaimsMaps)
auth := identity.NewAuthRecord(im, user.Subject(), authorizedScopes, nil, claims)
auth.SetUser(user)
return auth, true, nil
}
// Name implements the identity.Manager interface.
func (im *IdentifierIdentityManager) Name() string {
return im.identifier.Name()
}
// ScopesSupported implements the identity.Manager interface.
func (im *IdentifierIdentityManager) ScopesSupported(scopes map[string]bool) []string {
scopesSupported := make([]string, len(im.scopesSupported))
copy(scopesSupported, im.scopesSupported)
for _, scope := range im.identifier.ScopesSupported() {
scopesSupported = append(scopesSupported, scope)
}
return scopesSupported
}
// ClaimsSupported implements the identity.Manager interface.
func (im *IdentifierIdentityManager) ClaimsSupported(claims []string) []string {
return im.claimsSupported
}
// AddRoutes implements the identity.Manager interface.
func (im *IdentifierIdentityManager) AddRoutes(ctx context.Context, router *mux.Router) {
im.identifier.AddRoutes(ctx, router)
}
// OnSetLogon implements the identity.Manager interface.
func (im *IdentifierIdentityManager) OnSetLogon(cb func(ctx context.Context, rw http.ResponseWriter, user identity.User) error) error {
return im.identifier.OnSetLogon(cb)
}
// OnUnsetLogon implements the identity.Manager interface.
func (im *IdentifierIdentityManager) OnUnsetLogon(cb func(ctx context.Context, rw http.ResponseWriter) error) error {
return im.identifier.OnUnsetLogon(cb)
}