Initial QSfera import
This commit is contained in:
+475
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user