mirror of
https://github.com/opencloud-eu/opencloud.git
synced 2026-02-14 16:21:18 -05:00
563 lines
17 KiB
Go
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)
|
|
}
|