Initial QSfera import

This commit is contained in:
Курнат Андрей
2026-06-07 10:20:04 +03:00
commit 2315f25754
16485 changed files with 4826827 additions and 0 deletions
+475
View File
@@ -0,0 +1,475 @@
/*
* 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"
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"net/url"
"strings"
"time"
"github.com/golang-jwt/jwt/v5"
"github.com/gorilla/mux"
"github.com/libregraph/oidc-go"
"github.com/longsleep/rndm"
"github.com/sirupsen/logrus"
konnect "github.com/libregraph/lico"
"github.com/libregraph/lico/identity"
"github.com/libregraph/lico/managers"
"github.com/libregraph/lico/oidc/payload"
"github.com/libregraph/lico/version"
)
const cookieIdentityManagerName = "cookie"
// CookieIdentityManager implements an identity manager which passes through
// received HTTP cookies to a HTTP backend..
type CookieIdentityManager struct {
backendURI *url.URL
allowedCookies map[string]bool
scopesSupported []string
signInFormURI string
logger logrus.FieldLogger
client *http.Client
encryptionManager *EncryptionManager
}
// NewCookieIdentityManager creates a new CookieIdentityManager from the
// provided parameters.
func NewCookieIdentityManager(c *identity.Config, backendURI *url.URL, cookieNames []string, timeout time.Duration, transport http.RoundTripper) *CookieIdentityManager {
if transport == nil {
transport = http.DefaultTransport
}
client := &http.Client{
Timeout: timeout,
Transport: transport,
}
var allowedCookies map[string]bool
if len(cookieNames) != 0 {
allowedCookies = make(map[string]bool)
for _, n := range cookieNames {
allowedCookies[n] = true
}
}
im := &CookieIdentityManager{
backendURI: backendURI,
allowedCookies: allowedCookies,
signInFormURI: c.SignInFormURI.String(),
logger: c.Logger,
client: client,
scopesSupported: setupSupportedScopes([]string{
oidc.ScopeProfile,
oidc.ScopeEmail,
konnect.ScopeNumericID,
}, nil, c.ScopesSupported),
}
return im
}
// RegisterManagers registers the provided managers,
func (im *CookieIdentityManager) RegisterManagers(mgrs *managers.Managers) error {
im.encryptionManager = mgrs.Must("encryption").(*EncryptionManager)
return nil
}
type cookieUser struct {
raw string
name string
email string
id int64
claims jwt.MapClaims
}
func (u *cookieUser) Raw() string {
return u.raw
}
func (u *cookieUser) Subject() string {
sub, _ := getPublicSubject([]byte(u.raw), []byte(cookieIdentityManagerName))
return sub
}
func (u *cookieUser) Name() string {
return u.name
}
func (u *cookieUser) Email() string {
return u.email
}
func (u *cookieUser) EmailVerified() bool {
return false
}
func (u *cookieUser) ID() int64 {
return u.id
}
func (u *cookieUser) Claims() jwt.MapClaims {
return u.claims
}
type cookieBackendResponse struct {
Subject string `json:"sub"`
Name string `json:"name"`
Email string `json:"email"`
ID int64 `json:"id"`
}
func (im *CookieIdentityManager) backendRequest(ctx context.Context, encodedCookies string, headers http.Header) (*cookieUser, error) {
if encodedCookies == "" {
// Fastpath, do nothing when no cookies.
return nil, nil
}
request, err := http.NewRequest(http.MethodPost, im.backendURI.String(), nil)
if err != nil {
return nil, fmt.Errorf("failed to create request: %v", err)
}
// Copy over some request headers which are used for fingerprinting sessions.
request.Header.Set("Accept-Language", headers.Get("Accept-Language"))
request.Header.Set("User-Agent", headers.Get("User-Agent"))
request.Header.Set("Connection", "keep-alive") // XXX(longsleep): This is part of the Kopano Webapp finger print and we do not really know what the browser sent on sign-in :/
request.Header.Set("X-Konnect-Request", fmt.Sprintf("1/%s", version.Version))
request.Header.Set("Cookie", encodedCookies)
request = request.WithContext(ctx)
response, err := im.client.Do(request)
if err != nil {
return nil, fmt.Errorf("request failed: %v", err)
}
body, err := ioutil.ReadAll(response.Body)
response.Body.Close()
if err != nil {
return nil, fmt.Errorf("read response failed: %v", err)
}
switch response.StatusCode {
case http.StatusOK:
fallthrough
case http.StatusAccepted:
// breaks
case http.StatusUnauthorized:
fallthrough
case http.StatusForbidden:
// Not signed in.
return nil, nil
default:
return nil, fmt.Errorf("request returned error code: %v", response.StatusCode)
}
payload := &cookieBackendResponse{}
err = json.Unmarshal(body, payload)
if err != nil {
return nil, fmt.Errorf("failed to parse response: %v", err)
}
encryptedCookies, err := im.encryptionManager.EncryptStringToHexString(encodedCookies)
if err != nil {
return nil, fmt.Errorf("failed to encrypt cookies: %v", err)
}
claims := make(jwt.MapClaims)
claims["cookie.v"] = encryptedCookies
claims["cookie.al"] = headers.Get("Accept-Language")
claims["cookie.ua"] = headers.Get("User-Agent")
claims[konnect.IdentifiedUserIDClaim] = payload.Subject
user := &cookieUser{
raw: payload.Subject,
email: payload.Email,
name: payload.Name,
id: payload.ID,
claims: claims,
}
return user, nil
}
// Authenticate implements the identity.Manager interface.
func (im *CookieIdentityManager) Authenticate(ctx context.Context, rw http.ResponseWriter, req *http.Request, ar *payload.AuthenticationRequest, next identity.Manager) (identity.AuthRecord, error) {
// Process incoming cookies, filter, and encode to string.
var encodedCookies []string
for _, cookie := range req.Cookies() {
if im.allowedCookies != nil {
if allowed, _ := im.allowedCookies[cookie.Name]; !allowed {
continue
}
}
encodedCookies = append(encodedCookies, cookie.String())
}
encodedCookiesString := strings.Join(encodedCookies, "; ")
user, err := im.backendRequest(ctx, encodedCookiesString, req.Header)
if err != nil {
// Error, directly return.
im.logger.Errorln("CookieIdentityManager: backend request error", err)
return nil, ar.NewError(oidc.ErrorCodeOAuth2ServerError, "CookieIdentityManager: backend request error")
}
if user == nil {
// Not signed in.
err = ar.NewError(oidc.ErrorCodeOIDCLoginRequired, "CookieIdentityManager: 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, "CookieIdentityManager: prompt=login request")
}
case ar.Prompts[oidc.PromptSelectAccount] == true:
// Not supported, just ignore.
fallthrough
default:
// Let all other prompt values pass.
}
// 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 err != nil {
u, _ := url.Parse(im.signInFormURI)
return nil, identity.NewLoginRequiredError(err.Error(), u)
}
auth := identity.NewAuthRecord(im, user.Subject(), nil, nil, nil)
auth.SetUser(user)
return auth, nil
}
// Authorize implements the identity.Manager interface.
func (im *CookieIdentityManager) 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.
}
// Fastpath for known clients.
switch ar.ClientID {
default:
// TODO(longsleep): Implement previous consent checks via backend.
approvedScopes = ar.Scopes
}
// Offline access validation.
// http://openid.net/specs/openid-connect-core-1_0.html#OfflineAccess
if ok, _ := ar.Scopes[oidc.ScopeOfflineAccess]; ok {
if !promptConsent {
// 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,
delete(ar.Scopes, oidc.ScopeOfflineAccess)
im.logger.Debugln("consent is required for offline access but not given, removed offline_access scope")
} else {
// NOTE(longsleep): Cookie identity relies on the presence of session cookies know to a backend. Thus offline access is not supported.
im.logger.Warnf("CookieIdentityManager: offline_access requested but not supported, removed offline_access scope")
delete(ar.Scopes, oidc.ScopeOfflineAccess)
}
}
if promptConsent {
if ar.Prompts[oidc.PromptNone] == true {
return auth, ar.NewError(oidc.ErrorCodeOIDCInteractionRequired, "consent required")
}
// TODO(longsleep): Implement permissions page / consent prompt.
return auth, ar.NewError(oidc.ErrorCodeOIDCInteractionRequired, "consent required")
}
auth.AuthorizeScopes(approvedScopes)
auth.AuthorizeClaims(ar.Claims)
return auth, nil
}
// EndSession implements the identity.Manager interface.
func (im *CookieIdentityManager) EndSession(ctx context.Context, rw http.ResponseWriter, req *http.Request, ar *payload.EndSessionRequest) error {
// XXX
return nil
}
// ApproveScopes implements the Backend interface.
func (im *CookieIdentityManager) 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 *CookieIdentityManager) ApprovedScopes(ctx context.Context, sub string, audience string, ref string) (map[string]bool, error) {
if ref == "" {
return nil, fmt.Errorf("SimplePasswdBackend: invalid ref")
}
return nil, nil
}
// Fetch implements the identity.Manager interface.
func (im *CookieIdentityManager) Fetch(ctx context.Context, userID string, sessionRef *string, scopes map[string]bool, requestedClaimsMaps []*payload.ClaimsRequestMap, requestedScopes map[string]bool) (identity.AuthRecord, bool, error) {
var user identity.PublicUser
// Try identty from context.
auth, _ := identity.FromContext(ctx)
if auth != nil {
if auth.User().Raw() != userID {
return nil, false, fmt.Errorf("CookieIdentityManager: wrong user - this should not happen")
}
user = auth.User() // This gets the user when added during Authenticate.
}
if user == nil {
// Try claims from context.
claims, _ := konnect.FromClaimsContext(ctx)
if claims != nil {
var identityClaimsMap jwt.MapClaims
switch c := claims.(type) {
case *konnect.AccessTokenClaims:
identityClaimsMap = c.IdentityClaims
case *konnect.RefreshTokenClaims:
identityClaimsMap = c.IdentityClaims
case jwt.MapClaims:
identityClaimsMap = c
default:
return nil, false, fmt.Errorf("CookieIdentityManager: unknown identity claims type")
}
var err error
var encodedCookies string
encryptedCookies, _ := identityClaimsMap["cookie.v"].(string)
if encryptedCookies != "" {
encodedCookies, err = im.encryptionManager.DecryptHexToString(encryptedCookies)
if err != nil {
return nil, false, fmt.Errorf("CookieIdentityManager: %v", err)
}
} else {
encodedCookies = encryptedCookies
}
headers := http.Header{}
if al, ok := identityClaimsMap["cookie.al"]; ok {
headers.Set("Accept-Language", al.(string))
}
if ua, ok := identityClaimsMap["cookie.ua"]; ok {
headers.Set("User-Agent", ua.(string))
}
user, err = im.backendRequest(ctx, encodedCookies, headers)
if err != nil {
// Error, directly return.
im.logger.WithError(err).Errorln("CookieIdentityManager: backend request error")
return nil, false, fmt.Errorf("CookieIdentityManager: backend request error")
}
}
}
if user == nil {
return nil, false, fmt.Errorf("CookieIdentityManager: no user")
}
if user.Raw() != userID {
return nil, false, fmt.Errorf("CookieIdentityManager: wrong user")
}
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 *CookieIdentityManager) Name() string {
return cookieIdentityManagerName
}
// ScopesSupported implements the identity.Manager interface.
func (im *CookieIdentityManager) ScopesSupported(scopes map[string]bool) []string {
return im.scopesSupported
}
// ClaimsSupported implements the identity.Manager interface.
func (im *CookieIdentityManager) ClaimsSupported(claims []string) []string {
return []string{
oidc.NameClaim,
oidc.EmailClaim,
oidc.EmailVerifiedClaim,
}
}
// AddRoutes implements the identity.Manager interface.
func (im *CookieIdentityManager) AddRoutes(ctx context.Context, router *mux.Router) {
}
// OnSetLogon implements the identity.Manager interface.
func (im *CookieIdentityManager) OnSetLogon(func(ctx context.Context, rw http.ResponseWriter, user identity.User) error) error {
return nil
}
// OnUnsetLogon implements the identity.Manager interface.
func (im *CookieIdentityManager) OnUnsetLogon(func(ctx context.Context, rw http.ResponseWriter) error) error {
return nil
}
+228
View File
@@ -0,0 +1,228 @@
/*
* 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"
"strings"
"github.com/golang-jwt/jwt/v5"
"github.com/gorilla/mux"
"github.com/libregraph/oidc-go"
"github.com/longsleep/rndm"
konnect "github.com/libregraph/lico"
"github.com/libregraph/lico/identity"
"github.com/libregraph/lico/oidc/payload"
)
const dummyIdentityManagerName = "dummy"
// DummyIdentityManager implements an identity manager which always grants
// access to a fixed user id.
type DummyIdentityManager struct {
sub string
scopesSupported []string
}
// NewDummyIdentityManager creates a new DummyIdentityManager from the
// provided parameters.
func NewDummyIdentityManager(c *identity.Config, sub string) *DummyIdentityManager {
im := &DummyIdentityManager{
sub: sub,
scopesSupported: setupSupportedScopes([]string{
oidc.ScopeProfile,
oidc.ScopeEmail,
}, nil, c.ScopesSupported),
}
return im
}
type dummyUser struct {
raw string
}
func (u *dummyUser) Raw() string {
return u.raw
}
func (u *dummyUser) Subject() string {
sub, _ := getPublicSubject([]byte(u.raw), []byte(dummyIdentityManagerName))
return sub
}
func (u *dummyUser) Email() string {
return fmt.Sprintf("%s@%s.local", u.raw, u.raw)
}
func (u *dummyUser) EmailVerified() bool {
return false
}
func (u *dummyUser) Name() string {
return fmt.Sprintf("Foo %s", strings.Title(u.raw))
}
func (u *dummyUser) Claims() jwt.MapClaims {
claims := make(jwt.MapClaims)
claims[konnect.IdentifiedUserIDClaim] = u.raw
return claims
}
// Authenticate implements the identity.Manager interface.
func (im *DummyIdentityManager) Authenticate(ctx context.Context, rw http.ResponseWriter, req *http.Request, ar *payload.AuthenticationRequest, next identity.Manager) (identity.AuthRecord, error) {
user := &dummyUser{im.sub}
// Check request.
err := ar.Verify(user.Subject())
if err != nil {
return nil, err
}
auth := identity.NewAuthRecord(im, user.Subject(), nil, nil, nil)
auth.SetUser(user)
return auth, nil
}
// Authorize implements the identity.Manager interface.
func (im *DummyIdentityManager) 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.
}
// TODO(longsleep): Move the code below to general function.
// TODO(longsleep): Validate scopes and force prompt.
approvedScopes = ar.Scopes
// Offline access validation.
// http://openid.net/specs/openid-connect-core-1_0.html#OfflineAccess
if ok, _ := ar.Scopes[oidc.ScopeOfflineAccess]; ok {
if !promptConsent {
// 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,
delete(ar.Scopes, oidc.ScopeOfflineAccess)
}
}
if promptConsent {
if ar.Prompts[oidc.PromptNone] == true {
return auth, ar.NewError(oidc.ErrorCodeOIDCInteractionRequired, "consent required")
}
// TODO(longsleep): Implement consent page.
return auth, ar.NewError(oidc.ErrorCodeOIDCInteractionRequired, "consent required, but page not implemented")
}
auth.AuthorizeScopes(approvedScopes)
auth.AuthorizeClaims(ar.Claims)
return auth, nil
}
// EndSession implements the identity.Manager interface.
func (im *DummyIdentityManager) EndSession(ctx context.Context, rw http.ResponseWriter, req *http.Request, esr *payload.EndSessionRequest) error {
user := &dummyUser{im.sub}
err := esr.Verify(user.Subject())
if err != nil {
return err
}
return nil
}
// ApproveScopes implements the Backend interface.
func (im *DummyIdentityManager) 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 *DummyIdentityManager) ApprovedScopes(ctx context.Context, sub string, audience string, ref string) (map[string]bool, error) {
if ref == "" {
return nil, fmt.Errorf("SimplePasswdBackend: invalid ref")
}
return nil, nil
}
// Fetch implements the identity.Manager interface.
func (im *DummyIdentityManager) Fetch(ctx context.Context, userID string, sessionRef *string, scopes map[string]bool, requestedClaimsMaps []*payload.ClaimsRequestMap, requestedScopes map[string]bool) (identity.AuthRecord, bool, error) {
if userID != im.sub {
return nil, false, fmt.Errorf("DummyIdentityManager: no user")
}
user := &dummyUser{im.sub}
authorizedScopes, _ := identity.AuthorizeScopes(im, user, scopes)
claims := identity.GetUserClaimsForScopes(user, authorizedScopes, requestedClaimsMaps)
return identity.NewAuthRecord(im, user.Subject(), authorizedScopes, nil, claims), true, nil
}
// Name implements the identity.Manager interface.
func (im *DummyIdentityManager) Name() string {
return dummyIdentityManagerName
}
// ScopesSupported implements the identity.Manager interface.
func (im *DummyIdentityManager) ScopesSupported(scopes map[string]bool) []string {
return im.scopesSupported
}
// ClaimsSupported implements the identity.Manager interface.
func (im *DummyIdentityManager) ClaimsSupported(claims []string) []string {
return []string{
oidc.NameClaim,
oidc.EmailClaim,
oidc.EmailVerifiedClaim,
}
}
// AddRoutes implements the identity.Manager interface.
func (im *DummyIdentityManager) AddRoutes(ctx context.Context, router *mux.Router) {
}
// OnSetLogon implements the identity.Manager interface.
func (im *DummyIdentityManager) OnSetLogon(func(ctx context.Context, rw http.ResponseWriter, user identity.User) error) error {
return nil
}
// OnUnsetLogon implements the identity.Manager interface.
func (im *DummyIdentityManager) OnUnsetLogon(func(ctx context.Context, rw http.ResponseWriter) error) error {
return nil
}
@@ -0,0 +1,113 @@
/*
* 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 (
"encoding/hex"
"fmt"
"github.com/libregraph/lico/encryption"
)
// EncryptionManager implements string encryption functions with a key.
type EncryptionManager struct {
key *[encryption.KeySize]byte
}
// NewEncryptionManager creates a new EncryptionManager with the provided key.
func NewEncryptionManager(key *[encryption.KeySize]byte) (*EncryptionManager, error) {
em := &EncryptionManager{
key: key,
}
return em, nil
}
// SetKey sets the provided key for the accociated manager.
func (em *EncryptionManager) SetKey(key []byte) error {
switch len(key) {
case encryption.KeySize:
// all good, breaks
case hex.EncodedLen(encryption.KeySize):
// try to decode with hex
dst := make([]byte, encryption.KeySize)
if _, err := hex.Decode(dst, key); err == nil {
key = dst
}
}
if len(key) != encryption.KeySize {
return fmt.Errorf("encryption key size error, is %d, want %d", len(key), encryption.KeySize)
}
em.key = new([encryption.KeySize]byte)
copy(em.key[:], key[:encryption.KeySize])
return nil
}
// GetKeySize returns the size of the accociated manager's key.
func (em *EncryptionManager) GetKeySize() int {
return len(em.key)
}
// EncryptStringToHexString encrypts a plaintext string with the accociated
// key and returns the hex encoded ciphertext as string.
func (em *EncryptionManager) EncryptStringToHexString(plaintext string) (string, error) {
ciphertext, err := em.Encrypt([]byte(plaintext))
if err != nil {
return "", err
}
return hex.EncodeToString(ciphertext), nil
}
// Encrypt encrypts plaintext []byte with the accociated key and returns
// ciphertext []byte.
func (em *EncryptionManager) Encrypt(plaintext []byte) ([]byte, error) {
ciphertext, err := encryption.Encrypt(plaintext, em.key)
if err != nil {
return nil, err
}
return ciphertext, nil
}
// DecryptHexToString decrypts a hex encoded string with the accociated key
// and returns the plain text as string.
func (em *EncryptionManager) DecryptHexToString(ciphertextHex string) (string, error) {
ciphertext, err := hex.DecodeString(ciphertextHex)
if err != nil {
return "", err
}
plaintext, err := em.Decrypt(ciphertext)
if err != nil {
return "", err
}
return string(plaintext), nil
}
// Decrypt decrypts ciphertext []byte with the accociated key and returns
// plaintext []byte.
func (em *EncryptionManager) Decrypt(ciphertext []byte) ([]byte, error) {
plaintext, err := encryption.Decrypt(ciphertext, em.key)
if err != nil {
return nil, err
}
return plaintext, nil
}
+503
View File
@@ -0,0 +1,503 @@
/*
* 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"
"github.com/golang-jwt/jwt/v5"
"github.com/gorilla/mux"
"github.com/libregraph/oidc-go"
"github.com/longsleep/rndm"
"github.com/sirupsen/logrus"
konnect "github.com/libregraph/lico"
"github.com/libregraph/lico/identity"
"github.com/libregraph/lico/identity/clients"
"github.com/libregraph/lico/managers"
"github.com/libregraph/lico/oidc/payload"
"github.com/libregraph/lico/utils"
)
const guestIdentitityManagerName = "guest"
// GuestIdentityManager implements an identity manager for guest users.
type GuestIdentityManager struct {
scopesSupported []string
claimsSupported []string
logger logrus.FieldLogger
clients *clients.Registry
onSetLogonCallbacks []func(ctx context.Context, rw http.ResponseWriter, user identity.User) error
onUnsetLogonCallbacks []func(ctx context.Context, rw http.ResponseWriter) error
}
// NewGuestIdentityManager creates a new GuestIdentityManager from the
// provided parameters.
func NewGuestIdentityManager(c *identity.Config) *GuestIdentityManager {
im := &GuestIdentityManager{
scopesSupported: setupSupportedScopes([]string{}, []string{
konnect.ScopeNumericID,
oidc.ScopeProfile,
oidc.ScopeEmail,
}, c.ScopesSupported),
claimsSupported: []string{
oidc.NameClaim,
oidc.FamilyNameClaim,
oidc.GivenNameClaim,
oidc.EmailClaim,
oidc.EmailVerifiedClaim,
},
logger: c.Logger,
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),
}
return im
}
type guestUser struct {
raw string
email string
emailVerified bool
name string
familyName string
givenName string
}
func newGuestUserFromClaims(claims jwt.MapClaims) *guestUser {
isGuestClaim, ok := claims[konnect.IdentifiedUserIsGuest]
if !ok {
return nil
}
isGuest, _ := isGuestClaim.(bool)
if !isGuest {
return nil
}
idClaim, ok := claims[konnect.IdentifiedUserIDClaim]
if !ok {
return nil
}
dataClaim, ok := claims[konnect.IdentifiedData]
if !ok {
return nil
}
user := &guestUser{
raw: idClaim.(string),
}
data, _ := dataClaim.(map[string]interface{})
for name, value := range data {
switch name {
case "e":
user.email, _ = value.(string)
case "ev":
if v, _ := value.(int); v == 1 {
user.emailVerified = true
}
case "n":
user.name, _ = value.(string)
case "nf":
user.familyName, _ = value.(string)
case "ng":
user.givenName, _ = value.(string)
}
}
return user
}
type minimalGuestUserData struct {
E string `json:"e,omitempty"`
EV int `json:"ev,omitempty"`
N string `json:"n,omitempty"`
NF string `json:"nf,omitempty"`
NG string `json:"ng,omitempty"`
}
func (u *guestUser) Raw() string {
return u.raw
}
func (u *guestUser) Subject() string {
sub, _ := getPublicSubject([]byte(u.raw), []byte(guestIdentitityManagerName))
return sub
}
func (u *guestUser) Email() string {
return u.email
}
func (u *guestUser) EmailVerified() bool {
return u.emailVerified
}
func (u *guestUser) Name() string {
return u.name
}
func (u *guestUser) FamilyName() string {
return u.familyName
}
func (u *guestUser) GivenName() string {
return u.givenName
}
func (u *guestUser) Claims() jwt.MapClaims {
claims := make(jwt.MapClaims)
claims[konnect.IdentifiedUserIDClaim] = u.raw
claims[konnect.IdentifiedUserIsGuest] = true
m := &minimalGuestUserData{
E: u.email,
N: u.name,
NF: u.familyName,
NG: u.givenName,
}
if u.emailVerified {
m.EV = 1
}
claims[konnect.IdentifiedData] = m
return claims
}
// RegisterManagers registers the provided managers,
func (im *GuestIdentityManager) RegisterManagers(mgrs *managers.Managers) error {
im.clients = mgrs.Must("clients").(*clients.Registry)
return nil
}
// Authenticate implements the identity.Manager interface.
func (im *GuestIdentityManager) Authenticate(ctx context.Context, rw http.ResponseWriter, req *http.Request, ar *payload.AuthenticationRequest, next identity.Manager) (identity.AuthRecord, error) {
// Check if required scopes are there.
if !ar.Scopes[konnect.ScopeGuestOK] {
return nil, ar.NewError(oidc.ErrorCodeOIDCLoginRequired, "GuestIdentityManager: required scope missing")
}
// Authenticate with signed client request object, so that must be there.
if ar.Request == nil {
return nil, ar.NewError(oidc.ErrorCodeOIDCInvalidRequestObject, "GuestIdentityManager: no request object")
}
// Further checks of signed claims.
roc, ok := ar.Request.Claims.(*payload.RequestObjectClaims)
if !ok {
return nil, ar.NewBadRequest(oidc.ErrorCodeOAuth2InvalidRequest, "GuestIdentityManager: invalid claims request")
}
// NOTE(longsleep): Require claims in request object to ensure that the
// claims requested come from there.
if roc.Claims == nil || ar.Claims == nil {
return nil, ar.NewError(oidc.ErrorCodeOAuth2InvalidRequest, "GuestIdentityManager: missing claims request")
}
// NOTE(longsleep): Guest mode requires ID token claims request with the
// guest claim set to an expected value.
if ar.Claims.IDToken == nil {
return nil, ar.NewError(oidc.ErrorCodeOAuth2InvalidRequest, "GuestIdentityManager: missing claims request for id_token")
}
guest, ok := ar.Claims.IDToken.GetStringValue("guest")
if !ok {
return nil, ar.NewError(oidc.ErrorCodeOAuth2InvalidRequest, "GuestIdentityManager: missing claim guest in id_token claims request")
}
// Ensure that request object claim is signed.
if ar.Request.Method == jwt.SigningMethodNone {
return nil, ar.NewBadRequest(oidc.ErrorCodeOIDCInvalidRequestObject, "GuestIdentityManager: request object must be signed")
}
if guest == "" {
return nil, ar.NewBadRequest(oidc.ErrorCodeOAuth2InvalidRequest, "GuestIdentityManager: invalid claim guest in id_token claims request")
}
// Additional email and profile claim values will be taken over into the
// guest user data.
email, _ := ar.Claims.IDToken.GetStringValue(oidc.EmailClaim)
var emailVerified bool
if emailVerifiedRaw, ok := ar.Claims.IDToken.Get(oidc.EmailVerifiedClaim); ok {
emailVerified, _ = emailVerifiedRaw.Value.(bool)
}
name, _ := ar.Claims.IDToken.GetStringValue(oidc.NameClaim)
familyName, _ := ar.Claims.IDToken.GetStringValue(oidc.FamilyNameClaim)
givenName, _ := ar.Claims.IDToken.GetStringValue(oidc.GivenNameClaim)
// Make new user with the provided signed information.
sub := guest
user := &guestUser{
raw: sub,
email: email,
emailVerified: emailVerified,
name: name,
familyName: familyName,
givenName: givenName,
}
// TODO(longsleep): Add additional claims to user from the claims request
// after filtering.
// Check request.
err := ar.Verify(user.Subject())
if err != nil {
return nil, err
}
auth := identity.NewAuthRecord(im, user.Subject(), nil, nil, nil)
auth.SetUser(user)
return auth, nil
}
// Authorize implements the identity.Manager interface.
func (im *GuestIdentityManager) 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.
}
// Offline access validation.
// http://openid.net/specs/openid-connect-core-1_0.html#OfflineAccess
if ok, _ := ar.Scopes[oidc.ScopeOfflineAccess]; ok {
if !promptConsent {
// 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,
delete(ar.Scopes, oidc.ScopeOfflineAccess)
}
}
// Authenticate with signed client request object, so that must be there.
if ar.Request == nil {
return nil, ar.NewError(oidc.ErrorCodeOIDCInvalidRequestObject, "GuestIdentityManager: authorize without request object")
}
// Further checks of signed claims.
roc, ok := ar.Request.Claims.(*payload.RequestObjectClaims)
if !ok {
return nil, ar.NewBadRequest(oidc.ErrorCodeOAuth2InvalidRequest, "GuestIdentityManager: authorize with invalid claims request")
}
securedDetails := roc.Secure()
if securedDetails == nil {
return nil, ar.NewBadRequest(oidc.ErrorCodeOIDCInvalidRequestObject, "GuestIdentityManager: authorize without secure client")
}
// TODO(longsleep): Validate scopes and force prompt.
if promptConsent {
if ar.Prompts[oidc.PromptNone] == true {
return auth, ar.NewError(oidc.ErrorCodeOIDCInteractionRequired, "consent required")
}
// TODO(longsleep): Implement consent page.
return auth, ar.NewError(oidc.ErrorCodeOIDCInteractionRequired, "consent required, but not supported for guests")
}
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 clientDetails.ID != securedDetails.ID {
return nil, ar.NewError(oidc.ErrorCodeOAuth2AccessDenied, "client mismatch")
}
// If not trusted we need to check request scopes.
if clientDetails.Trusted && securedDetails.TrustedScopes == nil {
// NOTE(longsleep): Guest scope validation takes all client provided
// scopes when the trusted client configuration has no trusted scopes
// configured. This can be used for fine grained access control using
// the trusted client configuration.
approvedScopes = ar.Scopes
} else {
supportedScopes := make(map[string]bool)
for _, scope := range im.ScopesSupported(nil) {
supportedScopes[scope] = true
}
// Auto approve all supported scopes.
approvedScopes = make(map[string]bool)
for scope := range ar.Scopes {
if _, ok := supportedScopes[scope]; ok {
approvedScopes[scope] = true
}
}
// Approve all additional scopes which are allowed by the trusted
// client.
for _, scope := range securedDetails.TrustedScopes {
if _, ok := ar.Scopes[scope]; ok {
approvedScopes[scope] = true
}
}
// Always approve openid scope.
if _, ok := ar.Scopes[oidc.ScopeOpenID]; ok {
approvedScopes[oidc.ScopeOpenID] = true
}
// Ensure that guest scope was approved.
if ok, _ := approvedScopes[konnect.ScopeGuestOK]; !ok {
return nil, ar.NewBadRequest(oidc.ErrorCodeOAuth2InvalidRequest, "GuestIdentityManager: client does not authorize "+konnect.ScopeGuestOK+" scope")
}
}
auth.AuthorizeScopes(approvedScopes)
auth.AuthorizeClaims(ar.Claims)
return auth, nil
}
// EndSession implements the identity.Manager interface.
func (im *GuestIdentityManager) EndSession(ctx context.Context, rw http.ResponseWriter, req *http.Request, esr *payload.EndSessionRequest) error {
// TODO(longsleep): Implement end session for guests.
// Trigger callbacks.
for _, f := range im.onUnsetLogonCallbacks {
err := f(ctx, rw)
if err != nil {
return err
}
}
return nil
}
// ApproveScopes implements the Backend interface.
func (im *GuestIdentityManager) 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 *GuestIdentityManager) ApprovedScopes(ctx context.Context, sub string, audience string, ref string) (map[string]bool, error) {
if ref == "" {
return nil, fmt.Errorf("GuestIdentityManager: invalid ref")
}
return nil, nil
}
// Fetch implements the identity.Manager interface.
func (im *GuestIdentityManager) Fetch(ctx context.Context, userID string, sessionRef *string, scopes map[string]bool, requestedClaimsMaps []*payload.ClaimsRequestMap, requestedScopes map[string]bool) (identity.AuthRecord, bool, error) {
var user identity.PublicUser
for {
// First check if current context has auth.
if auth, ok := identity.FromContext(ctx); ok {
user = auth.User()
break
}
// Second check if current context has claims with guest identity in it.
if claims, ok := konnect.FromClaimsContext(ctx); ok {
var identityClaims jwt.MapClaims
var identityProvider string
switch c := claims.(type) {
case *konnect.AccessTokenClaims:
identityClaims = c.IdentityClaims
identityProvider = c.IdentityProvider
case *konnect.RefreshTokenClaims:
identityClaims = c.IdentityClaims
identityProvider = c.IdentityProvider
}
if identityClaims != nil && identityProvider == im.Name() {
user = newGuestUserFromClaims(identityClaims)
break
}
}
return nil, false, fmt.Errorf("GuestIdentityManager: no user in context")
}
if user.Raw() != userID {
return nil, false, fmt.Errorf("GuestIdentityManager: wrong user")
}
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 *GuestIdentityManager) Name() string {
return guestIdentitityManagerName
}
// ScopesSupported implements the identity.Manager interface.
func (im *GuestIdentityManager) ScopesSupported(scopes map[string]bool) []string {
if scopes != nil {
// NOTE(longsleep): Allow scopes as we get them, since we already validated
// them in authorize.
supported := make([]string, 0)
for scope, ok := range scopes {
if ok {
supported = append(supported, scope)
}
}
return supported
}
return im.scopesSupported
}
// ClaimsSupported implements the identity.Manager interface.
func (im *GuestIdentityManager) ClaimsSupported(claims []string) []string {
return im.claimsSupported
}
// AddRoutes implements the identity.Manager interface.
func (im *GuestIdentityManager) AddRoutes(ctx context.Context, router *mux.Router) {
}
// OnSetLogon implements the identity.Manager interface.
func (im *GuestIdentityManager) OnSetLogon(cb func(ctx context.Context, rw http.ResponseWriter, user identity.User) error) error {
im.onSetLogonCallbacks = append(im.onSetLogonCallbacks, cb)
return nil
}
// OnUnsetLogon implements the identity.Manager interface.
func (im *GuestIdentityManager) OnUnsetLogon(cb func(ctx context.Context, rw http.ResponseWriter) error) error {
im.onUnsetLogonCallbacks = append(im.onUnsetLogonCallbacks, cb)
return nil
}
@@ -0,0 +1,562 @@
/*
* 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)
}
+52
View File
@@ -0,0 +1,52 @@
/*
* 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 (
"encoding/base64"
"golang.org/x/crypto/blake2b"
konnectoidc "github.com/libregraph/lico/oidc"
)
func setupSupportedScopes(scopes []string, extra []string, override []string) []string {
if len(override) > 0 {
return override
}
return append(scopes, extra...)
}
func getPublicSubject(sub []byte, extra []byte) (string, error) {
// Hash the raw subject with our specific salt.
hasher, err := blake2b.New512([]byte(konnectoidc.LibreGraphIDTokenSubjectSaltV1))
if err != nil {
return "", err
}
hasher.Write(sub)
hasher.Write([]byte(" "))
hasher.Write(extra)
// NOTE(longsleep): URL safe encoding for subject is important since many
// third party applications validate this with rather strict patterns. We
// also inject an @ to ensure its compatible to some apps which require one.
s := base64.RawURLEncoding.EncodeToString(hasher.Sum(nil))
return s[:16] + "@" + s[16:], nil
}