Files
opencloud/vendor/github.com/libregraph/lico/identity/managers/cookie.go
2023-04-19 20:24:34 +02:00

476 lines
14 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"
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"net/url"
"strings"
"time"
"github.com/golang-jwt/jwt/v4"
"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
}