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
+87
View File
@@ -0,0 +1,87 @@
/*
* 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 oidc
import (
"github.com/golang-jwt/jwt/v5"
)
// IDTokenClaims define the claims found in OIDC ID Tokens.
type IDTokenClaims struct {
jwt.RegisteredClaims
Nonce string `json:"nonce,omitempty"`
AuthTime int64 `json:"auth_time,omitempty"`
AccessTokenHash string `json:"at_hash,omitempty"`
CodeHash string `json:"c_hash,omitempty"`
*ProfileClaims
*EmailClaims
*SessionClaims
}
// ProfileClaims define the claims for the OIDC profile scope.
// https://openid.net/specs/openid-connect-basic-1_0.html#Scopes
type ProfileClaims struct {
jwt.RegisteredClaims
Name string `json:"name,omitempty"`
FamilyName string `json:"family_name,omitempty"`
GivenName string `json:"given_name,omitempty"`
PreferredUsername string `json:"preferred_username,omitempty"`
}
// NewProfileClaims return a new ProfileClaims set from the provided
// jwt.Claims or nil.
func NewProfileClaims(claims jwt.Claims) *ProfileClaims {
if claims == nil {
return nil
}
return claims.(*ProfileClaims)
}
// EmailClaims define the claims for the OIDC email scope.
// https://openid.net/specs/openid-connect-basic-1_0.html#Scopes
type EmailClaims struct {
jwt.RegisteredClaims
Email string `json:"email,omitempty"`
EmailVerified bool `json:"email_verified"`
}
// NewEmailClaims return a new EmailClaims set from the provided
// jwt.Claims or nil.
func NewEmailClaims(claims jwt.Claims) *EmailClaims {
if claims == nil {
return nil
}
return claims.(*EmailClaims)
}
// UserInfoClaims define the claims defined by the OIDC UserInfo
// endpoint.
type UserInfoClaims struct {
Subject string `json:"sub,omitempty"`
}
// SessionClaims define claims related to front end sessions, for example as
// specified by https://openid.net/specs/openid-connect-frontchannel-1_0.html
type SessionClaims struct {
SessionID string `json:"sid,omitempty"`
}
+36
View File
@@ -0,0 +1,36 @@
/*
* 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 code
import (
"github.com/libregraph/lico/identity"
"github.com/libregraph/lico/oidc/payload"
)
// Record bundles the data storedi in a code manager.
type Record struct {
AuthenticationRequest *payload.AuthenticationRequest
Auth identity.AuthRecord
Session *payload.Session
}
// Manager is a interface defining a code manager.
type Manager interface {
Create(record *Record) (string, error)
Pop(code string) (*Record, bool)
}
@@ -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 (
"context"
"time"
"github.com/longsleep/rndm"
"github.com/orcaman/concurrent-map"
"github.com/libregraph/lico/oidc/code"
)
const (
codeValidDuration = 2 * time.Minute
)
// Manager provides the api and state for OIDC code generation and token
// exchange. The CodeManager's methods are safe to call from multiple Go
// routines.
type memoryMapManager struct {
table cmap.ConcurrentMap
codeDuration time.Duration
}
type codeRequestRecord struct {
record *code.Record
//ar *payload.AuthenticationRequest
//auth identity.AuthRecord
when time.Time
}
// NewMemoryMapManager creates a new CodeManager.
func NewMemoryMapManager(ctx context.Context) code.Manager {
cm := &memoryMapManager{
table: cmap.New(),
}
// Cleanup function.
go func() {
ticker := time.NewTicker(30 * time.Second)
defer ticker.Stop()
for {
select {
case <-ticker.C:
cm.purgeExpired()
case <-ctx.Done():
return
}
}
}()
return cm
}
func (cm *memoryMapManager) purgeExpired() {
var expired []string
deadline := time.Now().Add(-codeValidDuration)
var record *codeRequestRecord
for entry := range cm.table.IterBuffered() {
record = entry.Val.(*codeRequestRecord)
if record.when.Before(deadline) {
expired = append(expired, entry.Key)
}
}
for _, code := range expired {
cm.table.Remove(code)
}
}
// Create creates a new random code string, stores it together with the provided
// values in the accociated CodeManager's table and returns the code.
func (cm *memoryMapManager) Create(record *code.Record) (string, error) {
code := rndm.GenerateRandomString(24)
rr := &codeRequestRecord{
record: record,
when: time.Now(),
}
cm.table.Set(code, rr)
return code, nil
}
// Pop looks up the provided code in the accociated CodeManagers's table. If
// found it returns the authentication request and backend record plus true.
// When not found, both values return as nil plus false.
func (cm *memoryMapManager) Pop(code string) (*code.Record, bool) {
stored, found := cm.table.Pop(code)
if !found {
return nil, false
}
rr := stored.(*codeRequestRecord)
return rr.record, true
}
+81
View File
@@ -0,0 +1,81 @@
/*
* 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 oidc
import (
"fmt"
"net/http"
"github.com/libregraph/lico/utils"
)
// OAuth2Error defines a general OAuth2 error with id and decription.
type OAuth2Error struct {
ErrorID string `json:"error"`
ErrorDescription string `json:"error_description"`
}
// Error implements the error interface.
func (err *OAuth2Error) Error() string {
return err.ErrorID
}
// Description implements the ErrorWithDescription interface.
func (err *OAuth2Error) Description() string {
return err.ErrorDescription
}
// NewOAuth2Error creates a new error with id and description.
func NewOAuth2Error(id string, description string) utils.ErrorWithDescription {
return &OAuth2Error{id, description}
}
// WriteWWWAuthenticateError writes the provided error with the provided
// http status code to the provided http response writer as a
// WWW-Authenticate header with comma separated fields for id and
// description.
func WriteWWWAuthenticateError(rw http.ResponseWriter, code int, err error) {
if code == 0 {
code = http.StatusUnauthorized
}
var description string
switch err.(type) {
case utils.ErrorWithDescription:
description = err.(utils.ErrorWithDescription).Description()
default:
}
rw.Header().Set("WWW-Authenticate", fmt.Sprintf("error=\"%s\", error_description=\"%s\"", err.Error(), description))
rw.WriteHeader(code)
}
// IsErrorWithID returns true if the given error is an OAuth2Error error with
// the given ID.
func IsErrorWithID(err error, id string) bool {
if err == nil {
return false
}
oauth2Error, ok := err.(*OAuth2Error)
if !ok {
return false
}
return oauth2Error.ErrorID == id
}
+22
View File
@@ -0,0 +1,22 @@
/*
* 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 oidc
// LibreGraphIDTokenSubjectSaltV1 is the salt value used when hashing Subjects
// in ID tokens created by this application.
const LibreGraphIDTokenSubjectSaltV1 = "lico-IDToken-v1"
+462
View File
@@ -0,0 +1,462 @@
/*
* 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 payload
import (
"fmt"
"net/http"
"net/url"
"strconv"
"strings"
"time"
"github.com/golang-jwt/jwt/v5"
"github.com/libregraph/oidc-go"
konnectoidc "github.com/libregraph/lico/oidc"
)
// AuthenticationRequest holds the incoming parameters and request data for
// the OpenID Connect 1.0 authorization endpoint as specified at
// http://openid.net/specs/openid-connect-core-1_0.html#AuthRequest and
// http://openid.net/specs/openid-connect-core-1_0.html#ImplicitAuthRequest
type AuthenticationRequest struct {
providerMetadata *oidc.WellKnown
RawScope string `schema:"scope"`
Claims *ClaimsRequest `schema:"claims"`
RawResponseType string `schema:"response_type"`
ResponseMode string `schema:"response_mode"`
ClientID string `schema:"client_id"`
RawRedirectURI string `schema:"redirect_uri"`
State string `schema:"state"`
Nonce string `schema:"nonce"`
RawPrompt string `schema:"prompt"`
RawIDTokenHint string `schema:"id_token_hint"`
RawMaxAge string `schema:"max_age"`
RawRequest string `schema:"request"`
RawRequestURI string `schema:"request_uri"`
RawRegistration string `schema:"registration"`
CodeChallenge string `schema:"code_challenge"`
CodeChallengeMethod string `schema:"code_challenge_method"`
Scopes map[string]bool `schema:"-"`
ResponseTypes map[string]bool `schema:"-"`
Prompts map[string]bool `schema:"-"`
RedirectURI *url.URL `schema:"-"`
IDTokenHint *jwt.Token `schema:"-"`
MaxAge time.Duration `schema:"-"`
Request *jwt.Token `schema:"-"`
UseFragment bool `schema:"-"`
Flow string `schema:"-"`
Session *Session `schema:"-"`
}
// DecodeAuthenticationRequest returns a AuthenticationRequest holding the
// provided requests form data.
func DecodeAuthenticationRequest(req *http.Request, providerMetadata *oidc.WellKnown, keyFunc jwt.Keyfunc) (*AuthenticationRequest, error) {
return NewAuthenticationRequest(req.Form, providerMetadata, keyFunc)
}
// NewAuthenticationRequest returns a AuthenticationRequest holding the
// provided url values.
func NewAuthenticationRequest(values url.Values, providerMetadata *oidc.WellKnown, keyFunc jwt.Keyfunc) (*AuthenticationRequest, error) {
ar := &AuthenticationRequest{
providerMetadata: providerMetadata,
Scopes: make(map[string]bool),
ResponseTypes: make(map[string]bool),
Prompts: make(map[string]bool),
}
err := DecodeSchema(ar, values)
if err != nil {
return nil, fmt.Errorf("failed to decode authentication request: %v", err)
}
if ar.RawScope != "" {
// Parse scope early, since the value is needed to handle the request
// parameter properly.
for _, scope := range strings.Split(ar.RawScope, " ") {
ar.Scopes[scope] = true
}
}
if ar.RawRequest != "" {
parser := &jwt.Parser{}
request, err := parser.ParseWithClaims(ar.RawRequest, &RequestObjectClaims{}, func(token *jwt.Token) (interface{}, error) {
if keyFunc != nil {
return keyFunc(token)
}
return nil, fmt.Errorf("Not validated")
})
if err != nil {
return nil, ar.NewBadRequest(oidc.ErrorCodeOIDCInvalidRequestObject, err.Error())
}
if claims, ok := request.Claims.(*RequestObjectClaims); ok {
err = ar.ApplyRequestObject(claims, request.Method)
if err != nil {
return nil, err
}
}
ar.Request = request
}
ar.RedirectURI, _ = url.Parse(ar.RawRedirectURI)
if ar.RawResponseType != "" {
for _, rt := range strings.Split(ar.RawResponseType, " ") {
ar.ResponseTypes[rt] = true
}
}
if ar.RawPrompt != "" {
for _, prompt := range strings.Split(ar.RawPrompt, " ") {
ar.Prompts[prompt] = true
}
}
switch ar.RawResponseType {
case oidc.ResponseTypeCode:
// Code flow.
ar.Flow = oidc.FlowCode
// breaks
case oidc.ResponseTypeIDToken:
// Implicit flow.
fallthrough
case oidc.ResponseTypeIDTokenToken:
// Implicit flow with access token.
ar.UseFragment = true
ar.Flow = oidc.FlowImplicit
case oidc.ResponseTypeCodeIDToken:
// Hybrid flow.
fallthrough
case oidc.ResponseTypeCodeToken:
// Hybgrid flow.
fallthrough
case oidc.ResponseTypeCodeIDTokenToken:
// Hybrid flow.
ar.UseFragment = true
ar.Flow = oidc.FlowHybrid
}
switch ar.ResponseMode {
case oidc.ResponseModeFragment:
ar.UseFragment = true
// breaks
case oidc.ResponseModeQuery:
ar.UseFragment = false
// breaks
}
if ar.RawMaxAge != "" {
maxAgeInt, err := strconv.ParseInt(ar.RawMaxAge, 10, 64)
if err != nil {
return nil, err
}
ar.MaxAge = time.Duration(maxAgeInt) * time.Second
}
if ar.Claims != nil && ar.Claims.Passthru != nil {
// Remove pass thru claims when not provided in a secure manner. This
// means that pass through claims can only be passed via a signed request
// objects and its claims.
if ar.Request == nil || ar.Request.Method == jwt.SigningMethodNone || ar.Request.Claims == nil {
ar.Claims.Passthru = nil
}
}
return ar, nil
}
// ApplyRequestObject applies the provided request object claims to the
// associated authentication request data with validation as required.
func (ar *AuthenticationRequest) ApplyRequestObject(roc *RequestObjectClaims, method jwt.SigningMethod) error {
// Basic consistency validation following spec at
// https://openid.net/specs/openid-connect-core-1_0.html#SignedRequestObject
if ok := ar.Scopes[oidc.ScopeOpenID]; !ok {
return ar.NewBadRequest(oidc.ErrorCodeOIDCInvalidRequestObject, "openid scope required when using the request parameter")
}
if roc.RawScope != "" {
ar.Scopes = make(map[string]bool)
// Parse scope directly, since the accociated authentication request
// has already parsed it when this is called.
for _, scope := range strings.Split(roc.RawScope, " ") {
ar.Scopes[scope] = true
}
}
if roc.RawResponseType != "" {
if roc.RawResponseType != ar.RawResponseType {
return ar.NewBadRequest(oidc.ErrorCodeOIDCInvalidRequestObject, "request object response_type mismatch")
}
}
if roc.ClientID != "" {
if roc.ClientID != ar.ClientID {
return ar.NewBadRequest(oidc.ErrorCodeOIDCInvalidRequestObject, "request object client_id mismatch")
}
}
if method != jwt.SigningMethodNone {
// Additional claim validation when signed. The spec says that iss and
// aud SHOULD have defined values. So for now we do not enforce here.
}
// Apply rest of the provided request object values to the accociated
// authentication request.
if roc.Claims != nil {
// NOTE(longsleep): Overwrite request claims with the signed claims
// from the request object. This ensures that only signed claims are
// processed if any have been given. If no signed claims have been
// given, the unsigned claims are kept, leaving it to further checks
// to ensure that only signed claims are used by checking that the
// roc object has claims.
ar.Claims = roc.Claims
}
if roc.RawRedirectURI != "" {
ar.RawRedirectURI = roc.RawRedirectURI
}
if roc.State != "" {
ar.State = roc.State
}
if roc.Nonce != "" {
ar.Nonce = roc.Nonce
}
if roc.RawPrompt != "" {
ar.RawPrompt = roc.RawPrompt
}
if roc.RawIDTokenHint != "" {
ar.RawIDTokenHint = roc.RawIDTokenHint
}
if roc.RawMaxAge != "" {
ar.RawMaxAge = roc.RawMaxAge
}
if roc.RawRegistration != "" {
ar.RawRegistration = roc.RawRegistration
}
if roc.CodeChallengeMethod != "" {
ar.CodeChallengeMethod = roc.CodeChallengeMethod
}
if roc.CodeChallenge != "" {
ar.CodeChallenge = roc.CodeChallenge
}
return nil
}
// Validate validates the request data of the accociated authentication request.
func (ar *AuthenticationRequest) Validate(keyFunc jwt.Keyfunc) error {
switch ar.RawResponseType {
case oidc.ResponseTypeCode:
// Code flow.
// breaks
case oidc.ResponseTypeCodeIDToken:
// Hybgrid flow.
if _, ok := ar.Scopes[oidc.ScopeOpenID]; !ok {
return ar.NewBadRequest(oidc.ErrorCodeOAuth2InvalidRequest, "missing openid scope in request")
}
// breaks
case oidc.ResponseTypeCodeToken:
// Hybgrid flow.
// breaks
case oidc.ResponseTypeCodeIDTokenToken:
// Hybgrid flow.
if _, ok := ar.Scopes[oidc.ScopeOpenID]; !ok {
return ar.NewBadRequest(oidc.ErrorCodeOAuth2InvalidRequest, "missing openid scope in request")
}
// breaks
case oidc.ResponseTypeIDToken:
// Implicit flow.
if _, ok := ar.Scopes[oidc.ScopeOpenID]; !ok {
return ar.NewBadRequest(oidc.ErrorCodeOAuth2InvalidRequest, "missing openid scope in request")
}
fallthrough
case oidc.ResponseTypeIDTokenToken:
// Implicit flow with access token.
if _, ok := ar.Scopes[oidc.ScopeOpenID]; !ok {
return ar.NewBadRequest(oidc.ErrorCodeOAuth2InvalidRequest, "missing openid scope in request")
}
if ar.Nonce == "" {
return ar.NewError(oidc.ErrorCodeOAuth2InvalidRequest, "nonce is required for implicit flow")
}
case oidc.ResponseTypeToken:
// OAuth2 flow implicit grant.
// breaks
default:
return ar.NewError(oidc.ErrorCodeOAuth2UnsupportedResponseType, "")
}
// Additional checks for flows with code.
if ar.Flow == oidc.FlowCode || ar.Flow == oidc.FlowHybrid {
switch ar.CodeChallengeMethod {
case "":
// breaks
case oidc.S256CodeChallengeMethod:
// breaks
case oidc.PlainCodeChallengeMethod:
// Plain is discouraged, and thus not supported.
fallthrough
default:
return ar.NewBadRequest(oidc.ErrorCodeOAuth2InvalidRequest, "transform algorithm not supported")
}
}
if _, hasNonePrompt := ar.Prompts[oidc.PromptNone]; hasNonePrompt {
if len(ar.Prompts) > 1 {
// Cannot have other prompts if none is requested.
return ar.NewError(oidc.ErrorCodeOAuth2InvalidRequest, "cannot request other prompts together with none")
}
}
if ar.ClientID == "" {
return ar.NewBadRequest(oidc.ErrorCodeOAuth2InvalidRequest, "missing client_id")
}
// TODO(longsleep): implement client_id white list.
if ar.RedirectURI == nil || !ar.RedirectURI.IsAbs() {
return ar.NewBadRequest(oidc.ErrorCodeOAuth2InvalidRequest, "invalid or missing redirect_uri")
}
if ar.RawIDTokenHint != "" {
parser := jwt.NewParser(jwt.WithoutClaimsValidation())
idTokenHint, err := parser.ParseWithClaims(ar.RawIDTokenHint, &konnectoidc.IDTokenClaims{}, func(token *jwt.Token) (interface{}, error) {
if keyFunc != nil {
return keyFunc(token)
}
return nil, fmt.Errorf("Not validated")
})
if err != nil {
return ar.NewBadRequest(oidc.ErrorCodeOAuth2InvalidRequest, err.Error())
}
ar.IDTokenHint = idTokenHint
}
// Offline access validation.
// http://openid.net/specs/openid-connect-core-1_0.html#OfflineAccess
if ok, _ := ar.Scopes[oidc.ScopeOfflineAccess]; ok {
if _, withCodeResponseType := ar.ResponseTypes[oidc.ResponseTypeCode]; !withCodeResponseType {
// Ignore the offline_access request unless the Client is using a
// response_type value that would result in an Authorization Code
// being returned.
delete(ar.Scopes, oidc.ScopeOfflineAccess)
}
}
if ar.RawRequestURI != "" {
return ar.NewError(oidc.ErrorCodeOIDCRequestURINotSupported, "")
}
if ar.RawRegistration != "" {
return ar.NewError(oidc.ErrorCodeOIDCRegistrationNotSupported, "")
}
return nil
}
// Verify checks that the passed parameters match the accociated requirements.
func (ar *AuthenticationRequest) Verify(userID string) error {
if ar.IDTokenHint != nil {
// Compare userID with IDTokenHint.
if userID != ar.IDTokenHint.Claims.(*konnectoidc.IDTokenClaims).Subject {
return ar.NewError(oidc.ErrorCodeOIDCLoginRequired, "userid mismatch")
}
}
return nil
}
// NewError creates a new error with id and string and the associated request's
// state.
func (ar *AuthenticationRequest) NewError(id string, description string) *AuthenticationError {
return &AuthenticationError{
ErrorID: id,
ErrorDescription: description,
State: ar.State,
}
}
// NewBadRequest creates a new error with id and string and the associated
// request's state.
func (ar *AuthenticationRequest) NewBadRequest(id string, description string) *AuthenticationBadRequest {
return &AuthenticationBadRequest{
ErrorID: id,
ErrorDescription: description,
State: ar.State,
}
}
// AuthenticationSuccess holds the outgoind data for a successful OpenID
// Connect 1.0 authorize request as specified at
// http://openid.net/specs/openid-connect-core-1_0.html#AuthResponse and
// http://openid.net/specs/openid-connect-core-1_0.html#ImplicitAuthResponse.
// https://openid.net/specs/openid-connect-session-1_0.html#CreatingUpdatingSessions
type AuthenticationSuccess struct {
Code string `url:"code,omitempty"`
AccessToken string `url:"access_token,omitempty"`
TokenType string `url:"token_type,omitempty"`
IDToken string `url:"id_token,omitempty"`
State string `url:"state"`
ExpiresIn int64 `url:"expires_in,omitempty"`
Scope string `url:"scope,omitempty"`
SessionState string `url:"session_state,omitempty"`
}
// AuthenticationError holds the outgoind data for a failed OpenID
// Connect 1.0 authorize request as specified at
// http://openid.net/specs/openid-connect-core-1_0.html#AuthError and
// http://openid.net/specs/openid-connect-core-1_0.html#ImplicitAuthError.
type AuthenticationError struct {
ErrorID string `url:"error" json:"error"`
ErrorDescription string `url:"error_description,omitempty" json:"error_description,omitempty"`
State string `url:"state,omitempty" json:"state,omitempty"`
}
// Error interface implementation.
func (ae *AuthenticationError) Error() string {
return ae.ErrorID
}
// Description implements ErrorWithDescription interface.
func (ae *AuthenticationError) Description() string {
return ae.ErrorDescription
}
// AuthenticationBadRequest holds the outgoing data for a failed OpenID Connect
// 1.0 authorize request with bad request parameters which make it impossible to
// continue with normal auth.
type AuthenticationBadRequest struct {
ErrorID string `url:"error" json:"error"`
ErrorDescription string `url:"error_description,omitempty" json:"error_description,omitempty"`
State string `url:"state,omitempty" json:"state,omitempty"`
}
// Error interface implementation.
func (ae *AuthenticationBadRequest) Error() string {
return ae.ErrorID
}
// Description implements ErrorWithDescription interface.
func (ae *AuthenticationBadRequest) Description() string {
return ae.ErrorDescription
}
+193
View File
@@ -0,0 +1,193 @@
/*
* 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 payload
import (
"encoding/json"
"strings"
"github.com/libregraph/oidc-go"
)
var scopedClaims = map[string]string{
oidc.NameClaim: oidc.ScopeProfile,
oidc.FamilyNameClaim: oidc.ScopeProfile,
oidc.GivenNameClaim: oidc.ScopeProfile,
oidc.MiddleNameClaim: oidc.ScopeProfile,
oidc.PreferredUsernameClaim: oidc.ScopeProfile,
oidc.ProfileClaim: oidc.ScopeProfile,
oidc.PictureClaim: oidc.ScopeProfile,
oidc.WebsiteClaim: oidc.ScopeProfile,
oidc.GenderClaim: oidc.ScopeProfile,
oidc.BirthdateClaim: oidc.ScopeProfile,
oidc.ZoneinfoClaim: oidc.ScopeProfile,
oidc.UpdatedAtClaim: oidc.ScopeProfile,
oidc.EmailClaim: oidc.ScopeEmail,
oidc.EmailVerifiedClaim: oidc.ScopeEmail,
}
// GetScopeForClaim returns the known scope if any for the provided claim name.
func GetScopeForClaim(claim string) (string, bool) {
scope, ok := scopedClaims[claim]
return scope, ok
}
// ClaimsRequest define the base claims structure for OpenID Connect claims
// request parameter value as specified at
// https://openid.net/specs/openid-connect-core-1_0.html#ClaimsParameter - in
// addition a Konnect specific pass thru value can be used to pass through any
// application specific values to access and reqfresh tokens.
type ClaimsRequest struct {
UserInfo *ClaimsRequestMap `json:"userinfo,omitempty"`
IDToken *ClaimsRequestMap `json:"id_token,omitempty"`
Passthru json.RawMessage `json:"passthru,omitempty"`
}
// ApplyScopes removes all claims requests from the accociated claims request
// which are not mapped to one of the provided approved scopes.
func (cr *ClaimsRequest) ApplyScopes(approvedScopes map[string]bool) error {
if cr.UserInfo != nil {
for claim := range *cr.UserInfo {
if approved := approvedScopes[scopedClaims[claim]]; !approved {
delete(*cr.UserInfo, claim)
}
}
}
if cr.IDToken != nil {
for claim := range *cr.IDToken {
if approved := approvedScopes[scopedClaims[claim]]; !approved {
delete(*cr.IDToken, claim)
}
}
}
return nil
}
// Scopes adds all scopes of the accociated claims requests claims to
// the provied scopes mapping safe the scopes already defined in the provided
// excluded scopes mapping.
func (cr *ClaimsRequest) Scopes(excludedScopes map[string]bool) []string {
scopesMap := make(map[string]bool)
if cr.UserInfo != nil {
for claim := range *cr.UserInfo {
scope := scopedClaims[claim]
if _, excluded := excludedScopes[scope]; !excluded {
scopesMap[scope] = true
}
}
}
if cr.IDToken != nil {
for claim := range *cr.IDToken {
scope := scopedClaims[claim]
if _, excluded := excludedScopes[scope]; !excluded {
scopesMap[scope] = true
}
}
}
scopes := make([]string, 0)
for scope := range scopesMap {
scopes = append(scopes, scope)
}
return scopes
}
// ClaimsRequestMap defines a mapping of claims request values used with
// OpenID Connect claims request parameter values.
type ClaimsRequestMap map[string]*ClaimsRequestValue
// ScopesMap returns a map of scopes defined by the claims in tha associated map.
func (crm *ClaimsRequestMap) ScopesMap(excludedScopes map[string]bool) map[string]bool {
scopesMap := make(map[string]bool)
for claim := range *crm {
scope := scopedClaims[claim]
if _, excluded := excludedScopes[scope]; !excluded {
scopesMap[scope] = true
}
}
return scopesMap
}
// Get returns the accociated maps claim value identified by the provided name.
func (crm ClaimsRequestMap) Get(claim string) (*ClaimsRequestValue, bool) {
value, ok := crm[claim]
return value, ok
}
// GetStringValue returns the accociated maps claim value identified by the
// provided name as string value.
func (crm ClaimsRequestMap) GetStringValue(claim string) (string, bool) {
value, ok := crm.Get(claim)
if !ok {
return "", false
}
s, ok := value.Value.(string)
return s, ok
}
// ClaimsRequestValue is the claims request detail definition of an OpenID
// Connect claims request parameter value.
type ClaimsRequestValue struct {
Essential bool `json:"essential,omitempty"`
Value interface{} `json:"value,omitempty"`
Values []interface{} `json:"values,omitempty"`
}
// Match returns true of the provided value is contained inside the accociated
// request values values or value.
func (crv *ClaimsRequestValue) Match(value interface{}) bool {
if len(crv.Values) == 0 {
return value == crv.Value
}
for _, v := range crv.Values {
if v == value {
return true
}
}
return false
}
// ScopesValue is a string array with JSON marshal to/from a space separated
// single string value.
type ScopesValue []string
func (sv ScopesValue) MarshalJSON() ([]byte, error) {
result := strings.Join(sv, " ")
return json.Marshal(&result)
}
func (sv *ScopesValue) UnmarshalJSON(data []byte) error {
var parsed string
err := json.Unmarshal(data, &parsed)
if err != nil {
return err
}
result := ScopesValue(strings.Split(parsed, " "))
*sv = result
return nil
}
+132
View File
@@ -0,0 +1,132 @@
/*
* 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 payload
import (
"fmt"
"net/http"
"net/url"
"github.com/golang-jwt/jwt/v5"
"github.com/libregraph/oidc-go"
konnectoidc "github.com/libregraph/lico/oidc"
)
// EndSessionRequest holds the incoming parameters and request data for OpenID
// Connect Session Management 1.0 RP initiaed logout requests as specified at
// https://openid.net/specs/openid-connect-session-1_0.html#RPLogout
type EndSessionRequest struct {
providerMetadata *oidc.WellKnown
RawIDTokenHint string `schema:"id_token_hint"`
RawPostLogoutRedirectURI string `schema:"post_logout_redirect_uri"`
State string `schema:"state"`
IDTokenHint *jwt.Token `schema:"-"`
PostLogoutRedirectURI *url.URL `schema:"-"`
}
// DecodeEndSessionRequest returns a EndSessionRequest holding the
// provided requests form data.
func DecodeEndSessionRequest(req *http.Request, providerMetadata *oidc.WellKnown) (*EndSessionRequest, error) {
return NewEndSessionRequest(req.Form, providerMetadata)
}
// NewEndSessionRequest returns a EndSessionRequest holding the
// provided url values.
func NewEndSessionRequest(values url.Values, providerMetadata *oidc.WellKnown) (*EndSessionRequest, error) {
esr := &EndSessionRequest{
providerMetadata: providerMetadata,
}
err := DecodeSchema(esr, values)
if err != nil {
return nil, err
}
esr.PostLogoutRedirectURI, _ = url.Parse(esr.RawPostLogoutRedirectURI)
return esr, nil
}
// Validate validates the request data of the accociated endSession request.
func (esr *EndSessionRequest) Validate(keyFunc jwt.Keyfunc) error {
if esr.RawIDTokenHint != "" {
parser := jwt.NewParser(jwt.WithoutClaimsValidation())
idTokenHint, err := parser.ParseWithClaims(esr.RawIDTokenHint, &konnectoidc.IDTokenClaims{}, func(token *jwt.Token) (interface{}, error) {
if keyFunc != nil {
return keyFunc(token)
}
return nil, fmt.Errorf("Not validated")
})
if err != nil {
return esr.NewBadRequest(oidc.ErrorCodeOAuth2InvalidRequest, err.Error())
}
esr.IDTokenHint = idTokenHint
}
return nil
}
// Verify checks that the passed parameters match the accociated requirements.
func (esr *EndSessionRequest) Verify(userID string) error {
if esr.IDTokenHint != nil {
// Compare userID with IDTokenHint.
if userID != esr.IDTokenHint.Claims.(*konnectoidc.IDTokenClaims).Subject {
return esr.NewBadRequest(oidc.ErrorCodeOAuth2InvalidRequest, "userid mismatch")
}
}
return nil
}
// NewError creates a new error with id and string and the associated request's
// state.
func (esr *EndSessionRequest) NewError(id string, description string) *AuthenticationError {
return &AuthenticationError{
ErrorID: id,
ErrorDescription: description,
State: esr.State,
}
}
// NewBadRequest creates a new error with id and string and the associated
// request's state.
func (esr *EndSessionRequest) NewBadRequest(id string, description string) *AuthenticationBadRequest {
return &AuthenticationBadRequest{
ErrorID: id,
ErrorDescription: description,
State: esr.State,
}
}
func (esr *EndSessionRequest) MakeRedirectEndSessionRequestURL() *url.URL {
if esr.PostLogoutRedirectURI == nil || esr.PostLogoutRedirectURI.String() == "" {
return nil
}
if esr.State == "" {
return esr.PostLogoutRedirectURI
}
uri, _ := url.Parse(esr.PostLogoutRedirectURI.String())
query := uri.Query()
query.Add("state", esr.State)
uri.RawQuery = query.Encode()
return uri
}
+315
View File
@@ -0,0 +1,315 @@
/*
* 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 payload
import (
"encoding/json"
"fmt"
"net/http"
"net/url"
"strings"
"github.com/golang-jwt/jwt/v5"
"github.com/libregraph/oidc-go"
"github.com/mendsley/gojwk"
"github.com/libregraph/lico/identity/clients"
konnectoidc "github.com/libregraph/lico/oidc"
)
// ClientRegistrationRequest holds the incoming request data for the OpenID
// Connect Dynamic Client Registration 1.0 client registration endpoint as
// specified at https://openid.net/specs/openid-connect-registration-1_0.html#ClientRegistration and
// https://openid.net/specs/openid-connect-session-1_0.html#DynRegRegistrations
type ClientRegistrationRequest struct {
RedirectURIs []string `json:"redirect_uris"`
ResponseTypes []string `json:"response_types"`
GrantTypes []string `json:"grant_types"`
ApplicationType string `json:"application_type"`
Contacts []string `json:"contacts"`
ClientName string `json:"client_name"`
ClientURI string `json:"client_uri"`
RawJWKS json.RawMessage `json:"jwks"`
RawIDTokenSignedResponseAlg string `json:"id_token_signed_response_alg"`
RawUserInfoSignedResponseAlg string `json:"userinfo_signed_response_alg"`
RawRequestObjectSigningAlg string `json:"request_object_signing_alg"`
RawTokenEndpointAuthMethod string `json:"token_endpoint_auth_method"`
RawTokenEndpointAuthSigningAlg string `json:"token_endpoint_auth_signing_alg"`
PostLogoutRedirectURIs []string `json:"post_logout_redirect_uris"`
JWKS *gojwk.Key `json:"-"`
}
// DecodeClientRegistrationRequest returns a ClientRegistrationRequest holding
// the provided request's data.
func DecodeClientRegistrationRequest(req *http.Request) (*ClientRegistrationRequest, error) {
contentType := req.Header.Get("Content-Type")
if !strings.HasPrefix(contentType, "application/json") {
return nil, fmt.Errorf("invalid content-type")
}
decoder := json.NewDecoder(req.Body)
var crr ClientRegistrationRequest
err := decoder.Decode(&crr)
if err != nil {
return nil, fmt.Errorf("failed to decode client registration request: %v", err)
}
if crr.RawJWKS != nil {
jwks, err := gojwk.Unmarshal(crr.RawJWKS)
if err != nil {
return nil, fmt.Errorf("failed to decode client registration request jwks: %v", err)
}
// Only use keys.
crr.JWKS = &gojwk.Key{
Keys: jwks.Keys,
}
}
return &crr, err
}
// Validate validates the request data of the accociated client registration
// request and fills in default data where required.
func (crr *ClientRegistrationRequest) Validate() error {
if len(crr.RedirectURIs) == 0 {
return konnectoidc.NewOAuth2Error(oidc.ErrorCodeOIDCInvalidRedirectURI, "redirect_uris required")
}
// Validate and filter response_type.
if len(crr.ResponseTypes) == 0 {
crr.ResponseTypes = []string{oidc.ResponseTypeCode}
}
requiredGrantTypes := make(map[string]bool)
responseTypes := make([]string, 0)
for _, responseType := range crr.ResponseTypes {
switch responseType {
case oidc.ResponseTypeCode:
requiredGrantTypes[oidc.GrantTypeAuthorizationCode] = true
responseTypes = append(responseTypes, responseType)
// breaks
case oidc.ResponseTypeCodeIDToken:
fallthrough
case oidc.ResponseTypeCodeIDTokenToken:
fallthrough
case oidc.ResponseTypeCodeToken:
requiredGrantTypes[oidc.GrantTypeAuthorizationCode] = true
requiredGrantTypes[oidc.GrantTypeImplicit] = true
responseTypes = append(responseTypes, responseType)
// breaks
case oidc.ResponseTypeIDToken:
fallthrough
case oidc.ResponseTypeIDTokenToken:
requiredGrantTypes[oidc.GrantTypeAuthorizationCode] = true
requiredGrantTypes[oidc.GrantTypeImplicit] = true
responseTypes = append(responseTypes, responseType)
// breaks
case oidc.ResponseTypeToken:
responseTypes = append(responseTypes, responseType)
default:
}
}
crr.ResponseTypes = responseTypes
// Filter and validate grant_types.
if len(crr.GrantTypes) == 0 {
crr.GrantTypes = []string{oidc.GrantTypeAuthorizationCode}
}
grantTypes := make([]string, 0)
registeredGrantTypes := make(map[string]bool)
for _, grantType := range crr.GrantTypes {
switch grantType {
case oidc.GrantTypeAuthorizationCode:
fallthrough
case oidc.GrantTypeImplicit:
fallthrough
case oidc.GrantTypeRefreshToken:
registeredGrantTypes[grantType] = true
grantTypes = append(grantTypes, grantType)
default:
}
}
for grantType := range requiredGrantTypes {
if ok := registeredGrantTypes[grantType]; !ok {
return konnectoidc.NewOAuth2Error(oidc.ErrorCodeOIDCInvalidClientMetadata, "grant_types conflict with response_types")
}
}
if crr.ApplicationType == "" {
crr.ApplicationType = oidc.ApplicationTypeWeb
}
switch crr.ApplicationType {
case oidc.ApplicationTypeWeb:
// Web Clients using the OAuth Implicit Grant Type MUST only register
// URLs using the https scheme as redirect_uris; they MUST NOT use
// localhost as the hostname.
for _, uriString := range crr.RedirectURIs {
uri, err := url.Parse(uriString)
if err != nil {
return konnectoidc.NewOAuth2Error(oidc.ErrorCodeOIDCInvalidRedirectURI, "failed to parse redirect_uris")
}
if ok := registeredGrantTypes[oidc.GrantTypeImplicit]; ok {
if uri.Scheme != "https" {
return konnectoidc.NewOAuth2Error(oidc.ErrorCodeOIDCInvalidRedirectURI, "web clients must use https redirect_uris")
}
if clients.IsLocalNativeHostURI(uri) {
return konnectoidc.NewOAuth2Error(oidc.ErrorCodeOIDCInvalidRedirectURI, "web clients must not use localhost redirect_uris")
}
}
}
case oidc.ApplicationTypeNative:
// Native Clients MUST only register redirect_uris using custom URI
// schemes or URLs using the http: scheme with localhost as the hostname.
for _, uriString := range crr.RedirectURIs {
uri, err := url.Parse(uriString)
if err != nil {
return konnectoidc.NewOAuth2Error(oidc.ErrorCodeOIDCInvalidRedirectURI, "failed to parse redirect_uris")
}
if !clients.IsLocalNativeHTTPURI(uri) {
return konnectoidc.NewOAuth2Error(oidc.ErrorCodeOIDCInvalidRedirectURI, "native clients must only use localhost redirect_uris with http")
}
}
default:
return konnectoidc.NewOAuth2Error(oidc.ErrorCodeOIDCInvalidClientMetadata, "unknown application_type")
}
if crr.RawIDTokenSignedResponseAlg == "" {
crr.RawIDTokenSignedResponseAlg = jwt.SigningMethodRS256.Alg()
}
if crr.RawIDTokenSignedResponseAlg != "" {
alg := jwt.GetSigningMethod(crr.RawIDTokenSignedResponseAlg)
if alg == nil {
return konnectoidc.NewOAuth2Error(oidc.ErrorCodeOIDCInvalidClientMetadata, "unknown id_token_signed_response_alg")
}
}
if crr.RawUserInfoSignedResponseAlg != "" {
alg := jwt.GetSigningMethod(crr.RawUserInfoSignedResponseAlg)
if alg == nil {
return konnectoidc.NewOAuth2Error(oidc.ErrorCodeOIDCInvalidClientMetadata, "unknown userinfo_signed_response_alg")
}
}
if crr.RawRequestObjectSigningAlg != "" {
alg := jwt.GetSigningMethod(crr.RawRequestObjectSigningAlg)
if alg == nil {
return konnectoidc.NewOAuth2Error(oidc.ErrorCodeOIDCInvalidClientMetadata, "unknown request_object_signing_alg")
}
}
if crr.RawTokenEndpointAuthMethod == "" {
crr.RawTokenEndpointAuthMethod = oidc.AuthMethodClientSecretBasic
}
if crr.RawTokenEndpointAuthMethod != "" {
switch crr.RawTokenEndpointAuthMethod {
case oidc.AuthMethodClientSecretBasic:
// breaks
case oidc.AuthMethodNone:
// breaks
default:
return konnectoidc.NewOAuth2Error(oidc.ErrorCodeOIDCInvalidClientMetadata, "unsupported token_endpoint_auth_method")
}
}
if crr.RawTokenEndpointAuthSigningAlg != "" {
alg := jwt.GetSigningMethod(crr.RawTokenEndpointAuthSigningAlg)
if alg == nil {
return konnectoidc.NewOAuth2Error(oidc.ErrorCodeOIDCInvalidClientMetadata, "unknown token_endpoint_auth_signing_alg")
}
}
for _, uriString := range crr.PostLogoutRedirectURIs {
_, err := url.Parse(uriString)
if err != nil {
return konnectoidc.NewOAuth2Error(oidc.ErrorCodeOIDCInvalidClientMetadata, "failed to parse post_logout_redirect_uris")
}
}
if crr.JWKS != nil {
if len(crr.JWKS.Keys) == 0 {
crr.JWKS = nil
} else {
enc := false
empty := true
for _, key := range crr.JWKS.Keys {
switch key.Use {
case "":
if enc {
return konnectoidc.NewOAuth2Error(oidc.ErrorCodeOIDCInvalidClientMetadata, "jwks includes enc key and unset use key")
}
empty = true
key.Use = "sig"
case "enc":
enc = true
if empty {
return konnectoidc.NewOAuth2Error(oidc.ErrorCodeOIDCInvalidClientMetadata, "jwks includes enc key and unset use key")
}
}
}
}
}
return nil
}
// ClientRegistration returns new dynamic client registration data for the
// accociated client registration request.
func (crr *ClientRegistrationRequest) ClientRegistration() (*clients.ClientRegistration, error) {
cr := &clients.ClientRegistration{
Contacts: crr.Contacts,
Name: crr.ClientName,
URI: crr.ClientURI,
GrantTypes: crr.GrantTypes,
ApplicationType: crr.ApplicationType,
RedirectURIs: crr.RedirectURIs,
JWKS: crr.JWKS,
RawIDTokenSignedResponseAlg: crr.RawIDTokenSignedResponseAlg,
RawUserInfoSignedResponseAlg: crr.RawUserInfoSignedResponseAlg,
RawRequestObjectSigningAlg: crr.RawRequestObjectSigningAlg,
RawTokenEndpointAuthMethod: crr.RawTokenEndpointAuthMethod,
RawTokenEndpointAuthSigningAlg: crr.RawTokenEndpointAuthSigningAlg,
PostLogoutRedirectURIs: crr.PostLogoutRedirectURIs,
}
return cr, nil
}
// ClientRegistrationResponse holds the outgoing data for a successful OpenID
// Connect Dynamic Client Registration 1.0 clientregistration request as
// specified at https://openid.net/specs/openid-connect-registration-1_0.html#RegistrationResponse
type ClientRegistrationResponse struct {
ClientID string `json:"client_id"`
ClientSecret string `json:"client_secret,omitempty"`
ClientIDIssuedAt int64 `json:"client_id_issued_at,omitempty"`
ClientSecretExpiresAt int64 `json:"client_secret_expires_at"`
// Include validated request data.
ClientRegistrationRequest
}
+69
View File
@@ -0,0 +1,69 @@
/*
* 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 payload
import (
"errors"
"github.com/golang-jwt/jwt/v5"
"github.com/libregraph/lico/identity/clients"
)
// RequestObjectClaims holds the incoming request object claims provided as
// JWT via request parameter to OpenID Connect 1.0 authorization endpoint
// requests specified at
// https://openid.net/specs/openid-connect-core-1_0.html#JWTRequests
type RequestObjectClaims struct {
jwt.RegisteredClaims
RawScope string `json:"scope"`
Claims *ClaimsRequest `json:"claims"`
RawResponseType string `json:"response_type"`
ResponseMode string `json:"response_mode"`
ClientID string `json:"client_id"`
RawRedirectURI string `json:"redirect_uri"`
State string `json:"state"`
Nonce string `json:"nonce"`
RawPrompt string `json:"prompt"`
RawIDTokenHint string `json:"id_token_hint"`
RawMaxAge string `json:"max_age"`
RawRegistration string `json:"registration"`
CodeChallenge string `json:"code_challenge"`
CodeChallengeMethod string `json:"code_challenge_method"`
client *clients.Secured
}
// SetSecure sets the provided client as owner of the accociated claims.
func (roc *RequestObjectClaims) SetSecure(client *clients.Secured) error {
if roc.ClientID != client.ID {
return errors.New("client ID mismatch")
}
roc.client = client
return nil
}
// Secure returns the accociated secure client or nil if not secure.
func (roc *RequestObjectClaims) Secure() *clients.Secured {
return roc.client
}
+55
View File
@@ -0,0 +1,55 @@
/*
* 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 payload
import (
"encoding/json"
"reflect"
"github.com/gorilla/schema"
)
var decoder = schema.NewDecoder()
var encoder = schema.NewEncoder()
// DecodeSchema decodes request form data into the provided dst schema struct.
func DecodeSchema(dst interface{}, src map[string][]string) error {
return decoder.Decode(dst, src)
}
// EncodeSchema encodes the provided src schema to the provided map.
func EncodeSchema(src interface{}, dst map[string][]string) error {
return encoder.Encode(src, dst)
}
// ConvertOIDCClaimsRequest is a converter function for oidc.ClaimsRequest data
// provided in URL schema.
func ConvertOIDCClaimsRequest(value string) reflect.Value {
v := ClaimsRequest{}
if err := json.Unmarshal([]byte(value), &v); err != nil {
return reflect.Value{}
}
return reflect.ValueOf(v)
}
func init() {
decoder.IgnoreUnknownKeys(true)
decoder.RegisterConverter(ClaimsRequest{}, ConvertOIDCClaimsRequest)
}
+30
View File
@@ -0,0 +1,30 @@
/*
* 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 payload
// Session defines a Provider's session with a String identifier for a Session.
// This represents a Session of a User Agent or device for a logged-in End-User
// at an RP. Different ID values are used to identify distinct sessions. This
// is implemented as defined in the OIDC Front Channel logout extension
// https://openid.net/specs/openid-connect-frontchannel-1_0.html#OPLogout
type Session struct {
Version int
ID string
Sub string
Provider string
}
+168
View File
@@ -0,0 +1,168 @@
/*
* 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 payload
import (
"encoding/base64"
"fmt"
"net/http"
"net/url"
"strings"
"github.com/golang-jwt/jwt/v5"
"github.com/libregraph/oidc-go"
konnectoidc "github.com/libregraph/lico/oidc"
)
// TokenRequest holds the incoming parameters and request data for
// the OpenID Connect 1.0 token endpoint as specified at
// http://openid.net/specs/openid-connect-core-1_0.html#TokenRequest
type TokenRequest struct {
providerMetadata *oidc.WellKnown
GrantType string `schema:"grant_type"`
Code string `schema:"code"`
RawRedirectURI string `schema:"redirect_uri"`
RawRefreshToken string `schema:"refresh_token"`
RawScope string `schema:"scope"`
ClientID string `schema:"client_id"`
ClientSecret string `schema:"client_secret"`
CodeVerifier string `schema:"code_verifier"`
RedirectURI *url.URL `schema:"-"`
RefreshToken *jwt.Token `schema:"-"`
Scopes map[string]bool `schema:"-"`
}
// DecodeTokenRequest return a TokenRequest holding the provided
// request's form data.
func DecodeTokenRequest(req *http.Request, providerMetadata *oidc.WellKnown) (*TokenRequest, error) {
tr, err := NewTokenRequest(req.PostForm, providerMetadata)
if err != nil {
return nil, err
}
var clientID string
var clientSecret string
auth := strings.SplitN(req.Header.Get("Authorization"), " ", 2)
switch auth[0] {
case "Basic":
// Support client_secret_basic authentication method.
if len(auth) != 2 {
return nil, fmt.Errorf("invalid Basic authorization header format")
}
var basic []byte
if basic, err = base64.StdEncoding.DecodeString(auth[1]); err != nil {
return nil, fmt.Errorf("invalid Basic authorization value: %w", err)
}
// Decode username as client ID and password as client secret. See
// https://tools.ietf.org/html/rfc6749#section-2.3.1 for details.
check := strings.SplitN(string(basic), ":", 2)
if len(check) == 2 {
// Data is encoded application/x-www-form-urlencoded UTF-8. See
// https://tools.ietf.org/html/rfc6749#appendix-B for details.
if clientID, err = url.QueryUnescape(check[0]); err == nil {
clientSecret, _ = url.QueryUnescape(check[1])
}
}
}
if tr.ClientID == "" {
if clientID == "" {
return nil, fmt.Errorf("client_id is missing")
}
// Use client ID and secret if no client_id was passed to the request directly.
tr.ClientID = clientID
tr.ClientSecret = clientSecret
} else if clientID != "" {
if tr.ClientID == clientID {
// Update the client secret, if the ID is a match. This replaces
// a directly given secret.
tr.ClientSecret = clientSecret
}
}
return tr, err
}
// NewTokenRequest returns a TokenRequest holding the provided url values.
func NewTokenRequest(values url.Values, providerMetadata *oidc.WellKnown) (*TokenRequest, error) {
tr := &TokenRequest{
providerMetadata: providerMetadata,
Scopes: make(map[string]bool),
}
err := DecodeSchema(tr, values)
if err != nil {
return nil, err
}
tr.RedirectURI, _ = url.Parse(tr.RawRedirectURI)
if tr.RawScope != "" {
for _, scope := range strings.Split(tr.RawScope, " ") {
tr.Scopes[scope] = true
}
}
return tr, nil
}
// Validate validates the request data of the accociated token request.
func (tr *TokenRequest) Validate(keyFunc jwt.Keyfunc, claims jwt.Claims) error {
switch tr.GrantType {
case oidc.GrantTypeAuthorizationCode:
// breaks
case oidc.GrantTypeRefreshToken:
if tr.RawRefreshToken != "" {
refreshToken, err := jwt.ParseWithClaims(tr.RawRefreshToken, claims, func(token *jwt.Token) (interface{}, error) {
if keyFunc != nil {
return keyFunc(token)
}
return nil, fmt.Errorf("Not validated")
})
if err != nil {
return konnectoidc.NewOAuth2Error(oidc.ErrorCodeOAuth2InvalidRequest, err.Error())
}
tr.RefreshToken = refreshToken
}
// breaks
default:
return konnectoidc.NewOAuth2Error(oidc.ErrorCodeOAuth2UnsupportedGrantType, "unsupported grant_type value")
}
return nil
}
// TokenSuccess holds the outgoing data for a successful OpenID
// Connect 1.0 token request as specified at
// http://openid.net/specs/openid-connect-core-1_0.html#TokenResponse.
type TokenSuccess struct {
AccessToken string `json:"access_token,omitempty"`
TokenType string `json:"token_type,omitempty"`
IDToken string `json:"id_token,omitempty"`
RefreshToken string `json:"refresh_token,omitempty"`
ExpiresIn int64 `json:"expires_in,omitempty"`
}
+30
View File
@@ -0,0 +1,30 @@
/*
* 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 payload
import (
"github.com/libregraph/lico/oidc"
)
// UserInfoResponse defines the data returned from the OIDC UserInfo
// endpoint.
type UserInfoResponse struct {
oidc.UserInfoClaims
*oidc.ProfileClaims
*oidc.EmailClaims
}
+40
View File
@@ -0,0 +1,40 @@
/*
* 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 payload
import (
"encoding/json"
)
// ToMap is a helper function to convert the provided payload struct to
// a map type which can be used to extend the payload data with additional fields.
func ToMap(payload interface{}) (map[string]interface{}, error) {
// NOTE(longsleep): This implementation sucks, marshal to JSON and unmarshal
// again - rly?
intermediate, err := json.Marshal(payload)
if err != nil {
return nil, err
}
claims := make(map[string]interface{})
err = json.Unmarshal(intermediate, &claims)
if err != nil {
return nil, err
}
return claims, nil
}
+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 provider
import (
"net/http"
"time"
"github.com/libregraph/lico/config"
)
// Config defines a Provider's configuration settings.
type Config struct {
Config *config.Config
IssuerIdentifier string
WellKnownPath string
JwksPath string
AuthorizationPath string
TokenPath string
UserInfoPath string
EndSessionPath string
CheckSessionIframePath string
RegistrationPath string
BrowserStateCookiePath string
BrowserStateCookieName string
BrowserStateCookieSameSite http.SameSite
SessionCookiePath string
SessionCookieName string
SessionCookieSameSite http.SameSite
AccessTokenDuration time.Duration
IDTokenDuration time.Duration
RefreshTokenDuration time.Duration
}
+93
View File
@@ -0,0 +1,93 @@
/*
* 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 provider
import (
"net/http"
)
func (p *Provider) setBrowserStateCookie(rw http.ResponseWriter, value string) error {
cookie := http.Cookie{
Name: p.browserStateCookieName,
Value: value,
Path: p.browserStateCookiePath,
Secure: true,
HttpOnly: false, // This Cookie is intended to be read by Javascript.
SameSite: p.browserStateCookieSameSite,
}
http.SetCookie(rw, &cookie)
return nil
}
func (p *Provider) removeBrowserStateCookie(rw http.ResponseWriter) error {
cookie := http.Cookie{
Name: p.browserStateCookieName,
Path: p.browserStateCookiePath,
Secure: true,
HttpOnly: false, // This Cookie is intended to be read by Javascript.
SameSite: p.browserStateCookieSameSite,
Expires: farPastExpiryTime,
}
http.SetCookie(rw, &cookie)
return nil
}
func (p *Provider) setSessionCookie(rw http.ResponseWriter, value string) error {
cookie := http.Cookie{
Name: p.sessionCookieName,
Value: value,
Path: p.sessionCookiePath,
Secure: true,
HttpOnly: true,
SameSite: p.sessionCookieSameSite,
}
http.SetCookie(rw, &cookie)
return nil
}
func (p *Provider) getSessionCookie(req *http.Request) (string, error) {
cookie, err := req.Cookie(p.sessionCookieName)
if err != nil {
return "", err
}
return cookie.Value, nil
}
func (p *Provider) removeSessionCookie(rw http.ResponseWriter) error {
cookie := http.Cookie{
Name: p.sessionCookieName,
Path: p.sessionCookiePath,
Secure: true,
HttpOnly: true,
SameSite: p.sessionCookieSameSite,
Expires: farPastExpiryTime,
}
http.SetCookie(rw, &cookie)
return nil
}
+947
View File
@@ -0,0 +1,947 @@
/*
* Copyright 2017-2021 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 provider
import (
"context"
"fmt"
"net/http"
"strings"
"github.com/go-jose/go-jose/v3"
"github.com/golang-jwt/jwt/v5"
"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"
konnectoidc "github.com/libregraph/lico/oidc"
"github.com/libregraph/lico/oidc/code"
"github.com/libregraph/lico/oidc/payload"
"github.com/libregraph/lico/utils"
)
const (
registrationSizeLimit = 1024 * 512
)
// WellKnownHandler implements the HTTP provider configuration endpoint
// for OpenID Connect 1.0 as specified at https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderConfig
func (p *Provider) WellKnownHandler(rw http.ResponseWriter, req *http.Request) {
// TODO(longsleep): Add caching headers.
wellKnown := p.metadata
err := utils.WriteJSON(rw, http.StatusOK, wellKnown, "")
if err != nil {
p.logger.WithError(err).Errorln("well-known request failed writing response")
}
}
// JwksHandler implements the HTTP provider JWKS endpoint for OpenID provider
// metadata used with OpenID Connect Discovery 1.0 as specified at https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderMetadata
func (p *Provider) JwksHandler(rw http.ResponseWriter, req *http.Request) {
addResponseHeaders(rw.Header())
validationKeys := p.validationKeys
jwks := &jose.JSONWebKeySet{
Keys: make([]jose.JSONWebKey, 0),
}
for kid, key := range validationKeys {
certificates, _ := p.certificates[kid]
keyJwk := jose.JSONWebKey{
Key: key,
KeyID: kid,
Use: "sig", // https://tools.ietf.org/html/rfc7517#section-4.2
Certificates: certificates,
}
if keyJwk.Valid() {
jwks.Keys = append(jwks.Keys, keyJwk.Public())
}
}
err := utils.WriteJSON(rw, http.StatusOK, jwks, "application/jwk-set+json")
if err != nil {
p.logger.WithError(err).Errorln("jwks request failed writing response")
}
}
// AuthorizeHandler implements the HTTP authorization endpoint for OpenID
// Connect 1.0 as specified at http://openid.net/specs/openid-connect-core-1_0.html#ImplicitAuthorizationEndpoint
//
// Currently AuthorizeHandler implements only the Implicit Flow as specified at
// http://openid.net/specs/openid-connect-core-1_0.html#ImplicitFlowAuth
func (p *Provider) AuthorizeHandler(rw http.ResponseWriter, req *http.Request) {
var err error
var auth identity.AuthRecord
addResponseHeaders(rw.Header())
ctx := konnect.NewRequestContext(req.Context(), req)
// OpenID Connect 1.0 authentication request validation.
// http://openid.net/specs/openid-connect-core-1_0.html#ImplicitValidation
err = req.ParseForm()
if err != nil {
p.logger.WithError(err).Errorln("authorize request invalid form data")
p.ErrorPage(rw, http.StatusBadRequest, oidc.ErrorCodeOAuth2InvalidRequest, err.Error())
return
}
ar, err := payload.DecodeAuthenticationRequest(req, p.metadata, func(token *jwt.Token) (interface{}, error) {
if claims, ok := token.Claims.(*payload.RequestObjectClaims); ok {
// Validate signed request tokens according to spec defined at
// https://openid.net/specs/openid-connect-core-1_0.html#SignedRequestObject
registration, _ := p.clients.Get(ctx, claims.ClientID)
if registration != nil {
if registration.RawRequestObjectSigningAlg != "" {
if token.Method.Alg() != registration.RawRequestObjectSigningAlg {
return nil, fmt.Errorf("token alg does not match client registration")
}
}
if token.Method == jwt.SigningMethodNone {
// Request parameters do not need to be signed to be valid, so
// none is allowed in this special case.
return jwt.UnsafeAllowNoneSignatureType, nil
}
// Get secure client.
if registration.JWKS != nil {
secureClient, err := registration.Secure(token.Header[oidc.JWTHeaderKeyID])
if err != nil {
return nil, err
}
if err := claims.SetSecure(secureClient); err != nil {
return nil, err
}
return secureClient.PublicKey, err
}
return nil, fmt.Errorf("no client keys registered")
} else {
// Also allow, when client is not registered and the token is unsigned.
if token.Method == jwt.SigningMethodNone {
// Request parameters do not need to be signed to be valid, so
// none is allowed in this special case.
return jwt.UnsafeAllowNoneSignatureType, nil
}
}
}
return nil, fmt.Errorf("not validated")
})
if err != nil {
p.logger.WithFields(utils.ErrorAsFields(err)).Errorln("authorize request invalid request data")
p.ErrorPage(rw, http.StatusBadRequest, oidc.ErrorCodeOAuth2InvalidRequest, err.Error())
return
}
err = ar.Validate(func(token *jwt.Token) (interface{}, error) {
// Validator for incoming IDToken hints, looks up key.
return p.validateJWT(token)
})
if err != nil {
goto done
}
// Inject implicit scopes set by client registration.
if registration, _ := p.clients.Get(ctx, ar.ClientID); registration != nil {
err = registration.ApplyImplicitScopes(ar.Scopes)
if err != nil {
p.logger.WithError(err).Debugln("failed to apply implicit scopes")
}
}
// Find session if any, ignoring errors.
ar.Session, err = p.getSession(req)
if err != nil {
p.logger.WithError(err).Debugln("failed to decode client session")
}
// Authorization Server Authenticates End-User
// http://openid.net/specs/openid-connect-core-1_0.html#ImplicitAuthenticates
auth, err = p.identityManager.Authenticate(ctx, rw, req, ar, p.guestManager)
if err != nil {
goto done
}
// Additional validation based on requested ID token claims.
if ar.Claims != nil && ar.Claims.IDToken != nil {
// Validate sub claim request
// https://openid.net/specs/openid-connect-core-1_0.html#ImplicitValidation
if subRequest, ok := ar.Claims.IDToken.Get(oidc.SubjectIdentifierClaim); ok {
if !subRequest.Match(auth.Subject()) {
err = ar.NewError(oidc.ErrorCodeOAuth2AccessDenied, "sub claim request mismatch")
goto done
}
}
}
// Authorization Server Obtains End-User Consent/Authorization
// http://openid.net/specs/openid-connect-core-1_0.html#ImplicitConsent
auth, err = auth.Manager().Authorize(ctx, rw, req, ar, auth)
if err != nil {
goto done
}
done:
p.AuthorizeResponse(rw, req, ar, auth, err)
}
// AuthorizeResponse writes the result according to the provided parameters to
// the provided http.ResponseWriter.
func (p *Provider) AuthorizeResponse(rw http.ResponseWriter, req *http.Request, ar *payload.AuthenticationRequest, auth identity.AuthRecord, err error) {
var codeString string
var accessTokenString string
var idTokenString string
var authorizedScopes map[string]bool
var session *payload.Session
var ctx context.Context
if err != nil {
goto done
}
ctx = identity.NewContext(konnect.NewRequestContext(req.Context(), req), auth)
// Create session.
session, err = p.updateOrCreateSession(rw, req, ar, auth)
if err != nil {
goto done
}
authorizedScopes = auth.AuthorizedScopes()
// Create code when requested.
if _, ok := ar.ResponseTypes[oidc.ResponseTypeCode]; ok {
codeString, err = p.codeManager.Create(&code.Record{
AuthenticationRequest: ar,
Auth: auth,
Session: session,
})
if err != nil {
goto done
}
}
// Create access token when requested.
if _, ok := ar.ResponseTypes[oidc.ResponseTypeToken]; ok {
accessTokenString, err = p.makeAccessToken(ctx, ar.ClientID, auth, nil, nil)
if err != nil {
goto done
}
}
// Create ID token when requested and granted.
if authorizedScopes[oidc.ScopeOpenID] {
if _, ok := ar.ResponseTypes[oidc.ResponseTypeIDToken]; ok {
idTokenString, err = p.makeIDToken(ctx, ar, auth, session, accessTokenString, codeString, nil, nil)
if err != nil {
goto done
}
}
}
done:
// Always set browser state.
browserState, browserStateErr := p.makeBrowserState(ar, auth, err)
if browserStateErr != nil {
p.logger.WithError(err).Errorln("failed to make browser state")
}
if browserStateErr = p.setBrowserStateCookie(rw, browserState); browserStateErr != nil {
p.logger.WithError(err).Errorln("failed to set browser state cookie")
}
if err != nil {
switch err.(type) {
case *payload.AuthenticationError:
p.Found(rw, ar.RedirectURI, err, ar.UseFragment)
case *payload.AuthenticationBadRequest:
p.ErrorPage(rw, http.StatusBadRequest, err.Error(), err.(*payload.AuthenticationBadRequest).Description())
case *identity.RedirectError:
p.Found(rw, err.(*identity.RedirectError).RedirectURI(), nil, false)
case *identity.LoginRequiredError:
p.LoginRequiredPage(rw, req, err.(*identity.LoginRequiredError).SignInURI())
case *identity.IsHandledError:
// do nothing
case *konnectoidc.OAuth2Error:
err = ar.NewError(err.Error(), err.(*konnectoidc.OAuth2Error).Description())
p.Found(rw, ar.RedirectURI, err, ar.UseFragment)
default:
p.logger.WithFields(utils.ErrorAsFields(err)).Errorln("authorize request failed")
p.ErrorPage(rw, http.StatusInternalServerError, err.Error(), "well sorry, but there was a problem")
}
return
}
sessionState, sessionStateErr := p.makeSessionState(req, ar, browserState)
if sessionStateErr != nil {
p.logger.WithError(err).Errorln("failed to make session state")
}
authorizedScopesList := makeArrayFromBoolMap(authorizedScopes)
// Successful Authentication Response
// http://openid.net/specs/openid-connect-core-1_0.html#ImplicitAuthResponse
response := &payload.AuthenticationSuccess{
State: ar.State,
Scope: strings.Join(authorizedScopesList, " "),
SessionState: sessionState,
}
if codeString != "" {
response.Code = codeString
}
if accessTokenString != "" {
response.AccessToken = accessTokenString
response.TokenType = oidc.TokenTypeBearer
response.ExpiresIn = int64(p.accessTokenDuration.Seconds())
}
if idTokenString != "" {
response.IDToken = idTokenString
}
p.Found(rw, ar.RedirectURI, response, ar.UseFragment)
}
// TokenHandler implements the HTTP token endpoint for OpenID
// Connect 1.0 as specified at http://openid.net/specs/openid-connect-core-1_0.html#TokenEndpoint
func (p *Provider) TokenHandler(rw http.ResponseWriter, req *http.Request) {
var err error
var tr *payload.TokenRequest
var found bool
var ar *payload.AuthenticationRequest
var auth identity.AuthRecord
var session *payload.Session
var accessTokenString string
var idTokenString string
var refreshTokenString string
var refreshTokenClaims *konnect.RefreshTokenClaims
var approvedScopes map[string]bool
var authorizedScopes map[string]bool
var clientDetails *clients.Details
signinMethod := p.signingMethodDefault
rw.Header().Set("Cache-Control", "no-store")
rw.Header().Set("Pragma", "no-cache")
ctx := konnect.NewRequestContext(req.Context(), req)
// Validate request method
switch req.Method {
case http.MethodPost:
// breaks
default:
err = konnectoidc.NewOAuth2Error(oidc.ErrorCodeOAuth2InvalidRequest, "request must be sent with POST")
goto done
}
// Token Request Validation
// http://openid.net/specs/openid-connect-core-1_0.html#TokenRequestValidation
err = req.ParseForm()
if err != nil {
err = konnectoidc.NewOAuth2Error(oidc.ErrorCodeOAuth2InvalidRequest, err.Error())
goto done
}
tr, err = payload.DecodeTokenRequest(req, p.metadata)
if err != nil {
err = konnectoidc.NewOAuth2Error(oidc.ErrorCodeOAuth2InvalidRequest, err.Error())
goto done
}
err = tr.Validate(func(token *jwt.Token) (interface{}, error) {
// Validator for incoming refresh tokens, looks up key.
return p.validateJWT(token)
}, &konnect.RefreshTokenClaims{})
if err != nil {
goto done
}
// Additional validations according to https://tools.ietf.org/html/rfc6749#section-4.1.3
clientDetails, err = p.clients.Lookup(ctx, tr.ClientID, tr.ClientSecret, tr.RedirectURI, "", false)
if err != nil {
err = konnectoidc.NewOAuth2Error(oidc.ErrorCodeOAuth2AccessDenied, err.Error())
goto done
}
if clientDetails != nil && clientDetails.Registration != nil {
signinMethod = jwt.GetSigningMethod(clientDetails.Registration.RawIDTokenSignedResponseAlg)
}
switch tr.GrantType {
case oidc.GrantTypeAuthorizationCode:
codeRecord, codeRecordFound := p.codeManager.Pop(tr.Code)
if !codeRecordFound {
err = konnectoidc.NewOAuth2Error(oidc.ErrorCodeOAuth2InvalidGrant, "code not found")
goto done
}
ar = codeRecord.AuthenticationRequest
auth = codeRecord.Auth
session = codeRecord.Session
authorizedScopes = auth.AuthorizedScopes()
// Ensure that the authorization code was issued to the client id.
if ar.ClientID != tr.ClientID {
err = konnectoidc.NewOAuth2Error(oidc.ErrorCodeOAuth2InvalidGrant, "client_id mismatch")
goto done
}
// Ensure that the "redirect_uri" parameter is a match.
if ar.RawRedirectURI != tr.RawRedirectURI {
err = konnectoidc.NewOAuth2Error(oidc.ErrorCodeOAuth2InvalidGrant, "redirect_uri mismatch")
goto done
}
// Validate code challenge according to https://tools.ietf.org/html/rfc7636#section-4.6
if tr.CodeVerifier != "" || ar.CodeChallenge != "" {
if codeVerifierErr := oidc.ValidateCodeChallenge(ar.CodeChallenge, ar.CodeChallengeMethod, tr.CodeVerifier); codeVerifierErr != nil {
err = konnectoidc.NewOAuth2Error(oidc.ErrorCodeOAuth2InvalidGrant, codeVerifierErr.Error())
goto done
}
}
if _, ok := identity.FromContext(ctx); !ok {
ctx = identity.NewContext(ctx, auth)
}
case oidc.GrantTypeRefreshToken:
if tr.RefreshToken == nil {
err = konnectoidc.NewOAuth2Error(oidc.ErrorCodeOAuth2InvalidGrant, "missing refresh_token")
goto done
}
// Get claims from refresh token.
claims := tr.RefreshToken.Claims.(*konnect.RefreshTokenClaims)
// Ensure that the authorization code was issued to the client id.
if claims.Audience[0] != tr.ClientID {
err = konnectoidc.NewOAuth2Error(oidc.ErrorCodeOAuth2InvalidGrant, "client_id mismatch")
goto done
}
// TODO(longsleep): Compare standard claims issuer.
userID, sessionRef := p.getUserIDAndSessionRefFromClaims(&claims.RegisteredClaims, nil, claims.IdentityClaims)
if userID == "" {
err = konnectoidc.NewOAuth2Error(oidc.ErrorCodeOAuth2InvalidToken, "missing data in kc.identity claim")
goto done
}
ctx = konnect.NewClaimsContext(ctx, claims)
currentIdentityManager, claimsErr := p.getIdentityManagerFromClaims(claims.IdentityProvider, claims.IdentityClaims)
if claimsErr != nil {
err = claimsErr
goto done
}
// Lookup Ref values from backend.
approvedScopes, err = currentIdentityManager.ApprovedScopes(ctx, claims.Subject, tr.ClientID, claims.Ref)
if err != nil {
goto done
}
if approvedScopes == nil {
// Use approvals from token if backend did not say anything.
approvedScopes = make(map[string]bool)
for _, scope := range claims.ApprovedScopesList {
approvedScopes[scope] = true
}
}
if len(tr.Scopes) > 0 {
// Make sure all requested scopes are granted and limit authorized
// scopes to the requested scopes.
authorizedScopes = make(map[string]bool)
for scope := range tr.Scopes {
if !approvedScopes[scope] {
err = konnectoidc.NewOAuth2Error(oidc.ErrorCodeOAuth2InsufficientScope, "insufficient scope")
goto done
} else {
authorizedScopes[scope] = true
}
}
} else {
// Authorize all approved scopes when no scopes are in request.
authorizedScopes = approvedScopes
}
// Load user record from identitymanager, without any scopes or claims.
auth, found, err = currentIdentityManager.Fetch(ctx, userID, sessionRef, nil, nil, authorizedScopes)
if !found {
err = konnectoidc.NewOAuth2Error(oidc.ErrorCodeOAuth2InvalidGrant, "user not found")
goto done
}
if err != nil {
goto done
}
// Add authorized scopes.
auth.AuthorizeScopes(authorizedScopes)
// Add authorized claims from request.
auth.AuthorizeClaims(claims.ApprovedClaimsRequest)
// Create fake request for token generation.
ar = &payload.AuthenticationRequest{
ClientID: claims.Audience[0],
}
// Remember refresh token claims, for use in access and id token generators later on.
refreshTokenClaims = claims
default:
err = konnectoidc.NewOAuth2Error(oidc.ErrorCodeOAuth2UnsupportedGrantType, "grant_type value not implemented")
goto done
}
// Create access token.
accessTokenString, err = p.makeAccessToken(ctx, ar.ClientID, auth, signinMethod, refreshTokenClaims)
if err != nil {
goto done
}
switch tr.GrantType {
case oidc.GrantTypeAuthorizationCode, oidc.GrantTypeRefreshToken:
// Create ID token when not previously requested amd openid scope is authorized.
if !ar.ResponseTypes[oidc.ResponseTypeIDToken] && authorizedScopes[oidc.ScopeOpenID] {
idTokenString, err = p.makeIDToken(ctx, ar, auth, session, accessTokenString, "", signinMethod, refreshTokenClaims)
if err != nil {
goto done
}
}
// Create refresh token when granted.
if authorizedScopes[oidc.ScopeOfflineAccess] {
refreshTokenString, err = p.makeRefreshToken(ctx, ar.ClientID, auth, nil)
if err != nil {
goto done
}
}
}
done:
if err != nil {
switch err.(type) {
case *konnectoidc.OAuth2Error:
err = utils.WriteJSON(rw, http.StatusBadRequest, err, "")
if err != nil {
p.logger.WithError(err).Errorln("token request failed writing response")
return
}
default:
p.logger.WithFields(utils.ErrorAsFields(err)).Errorln("token request failed")
p.ErrorPage(rw, http.StatusInternalServerError, err.Error(), "well sorry, but there was a problem")
}
return
}
// Successful Token Response
// http://openid.net/specs/openid-connect-core-1_0.html#TokenResponse
response := &payload.TokenSuccess{}
if accessTokenString != "" {
response.AccessToken = accessTokenString
response.TokenType = oidc.TokenTypeBearer
response.ExpiresIn = int64(p.accessTokenDuration.Seconds())
}
if idTokenString != "" {
response.IDToken = idTokenString
}
if refreshTokenString != "" {
response.RefreshToken = refreshTokenString
}
err = utils.WriteJSON(rw, http.StatusOK, response, "")
if err != nil {
p.logger.WithError(err).Errorln("token request failed writing response")
}
}
// UserInfoHandler implements the HTTP userinfo endpoint for OpenID
// Connect 1.0 as specified at https://openid.net/specs/openid-connect-core-1_0.html#UserInfo
func (p *Provider) UserInfoHandler(rw http.ResponseWriter, req *http.Request) {
var err error
addResponseHeaders(rw.Header())
switch req.Method {
case http.MethodHead:
fallthrough
case http.MethodPost:
fallthrough
case http.MethodGet:
// pass
default:
return
}
// Parse and validate UserInfo request
// https://openid.net/specs/openid-connect-core-1_0.html#UserInfoRequest
claims, err := p.GetAccessTokenClaimsFromRequest(req)
if err != nil {
p.logger.WithFields(utils.ErrorAsFields(err)).Debugln("userinfo request unauthorized")
konnectoidc.WriteWWWAuthenticateError(rw, http.StatusUnauthorized, err)
return
}
var auth identity.AuthRecord
var found bool
var requestedClaimsMap []*payload.ClaimsRequestMap
var authorizedScopes map[string]bool
userID, sessionRef := p.getUserIDAndSessionRefFromClaims(&claims.RegisteredClaims, claims.SessionClaims, claims.IdentityClaims)
ctx := konnect.NewClaimsContext(konnect.NewRequestContext(req.Context(), req), claims)
currentIdentityManager, err := p.getIdentityManagerFromClaims(claims.IdentityProvider, claims.IdentityClaims)
if err != nil {
goto done
}
if userID == "" {
err = fmt.Errorf("missing data in identity claim")
goto done
}
if claims.AuthorizedClaimsRequest != nil && claims.AuthorizedClaimsRequest.UserInfo != nil {
requestedClaimsMap = []*payload.ClaimsRequestMap{claims.AuthorizedClaimsRequest.UserInfo}
}
authorizedScopes = claims.AuthorizedScopes()
auth, found, err = currentIdentityManager.Fetch(ctx, userID, sessionRef, authorizedScopes, requestedClaimsMap, authorizedScopes)
if err != nil {
p.logger.WithFields(utils.ErrorAsFields(err)).Errorln("identity manager fetch failed")
found = false
}
if !found {
p.logger.WithField("sub", claims.RegisteredClaims.Subject).Debugln("userinfo request user not found")
p.ErrorPage(rw, http.StatusNotFound, "", "user not found")
return
}
done:
if err != nil {
p.logger.WithFields(utils.ErrorAsFields(err)).Debugln("userinfo request invalid token")
konnectoidc.WriteWWWAuthenticateError(rw, http.StatusUnauthorized, konnectoidc.NewOAuth2Error(oidc.ErrorCodeOAuth2InvalidToken, err.Error()))
return
}
publicSubject, err := p.PublicSubjectFromAuth(auth)
if err != nil {
p.logger.WithFields(utils.ErrorAsFields(err)).Debugln("userinfo request failed to create subject")
p.ErrorPage(rw, http.StatusInternalServerError, "", err.Error())
return
}
response := &konnect.UserInfoResponse{
UserInfoResponse: &payload.UserInfoResponse{
UserInfoClaims: konnectoidc.UserInfoClaims{
Subject: publicSubject,
},
ProfileClaims: konnectoidc.NewProfileClaims(auth.Claims(oidc.ScopeProfile)[0]),
EmailClaims: konnectoidc.NewEmailClaims(auth.Claims(oidc.ScopeEmail)[0]),
},
}
// Helper to receive user from auth, but only once.
withUser := func() func() identity.User {
var u identity.User
var fetched bool
return func() identity.User {
if !fetched {
fetched = true
u = auth.User()
}
return u
}
}()
authorizedScopes = auth.AuthorizedScopes()
var user identity.User
// Include additional Konnect specific claims when corresponding scopes are authorized.
if ok, _ := authorizedScopes[konnect.ScopeNumericID]; ok {
user = withUser()
if userWithID, ok := user.(identity.UserWithID); ok {
claims := &konnect.NumericIDClaims{
NumericID: userWithID.ID(),
}
if userWithUsername, ok := user.(identity.UserWithUsername); ok {
claims.NumericIDUsername = userWithUsername.Username()
}
if claims.NumericIDUsername == "" {
claims.NumericIDUsername = user.Subject()
}
response.NumericIDClaims = claims
}
}
if ok, _ := authorizedScopes[konnect.ScopeUniqueUserID]; ok {
user = withUser()
if userWithUniqueID, ok := user.(identity.UserWithUniqueID); ok {
claims := &konnect.UniqueUserIDClaims{
UniqueUserID: userWithUniqueID.UniqueID(),
}
response.UniqueUserIDClaims = claims
}
}
// Create a map so additional user specific claims can be added.
responseAsMap, err := payload.ToMap(response)
if err != nil {
p.logger.WithFields(utils.ErrorAsFields(err)).Debugln("userinfo request failed to encode claims")
p.ErrorPage(rw, http.StatusInternalServerError, "", err.Error())
return
}
// Inject extra claims.
extraClaims := auth.Claims(konnect.InternalExtraAccessTokenClaimsClaim)[0]
if extraClaims != nil {
if extraClaimsMap, ok := extraClaims.(jwt.MapClaims); ok {
for claim, value := range extraClaimsMap {
responseAsMap[claim] = value
}
}
}
// Support returning signed user info if the registered client requested it
// as specified in https://openid.net/specs/openid-connect-core-1_0.html#UserInfoResponse and
// https://openid.net/specs/openid-connect-registration-1_0.html#ClientMetadata
registration, _ := p.clients.Get(ctx, claims.Audience[0])
if registration != nil {
if registration.RawUserInfoSignedResponseAlg != "" {
// Get alg.
alg := jwt.GetSigningMethod(registration.RawUserInfoSignedResponseAlg)
// Set extra claims.
responseAsMap[oidc.IssuerIdentifierClaim] = p.issuerIdentifier
responseAsMap[oidc.AudienceClaim] = registration.ID
tokenString, err := p.makeJWT(ctx, alg, jwt.MapClaims(responseAsMap))
if err != nil {
p.logger.WithFields(utils.ErrorAsFields(err)).Debugln("userinfo request failed to encode jwt")
p.ErrorPage(rw, http.StatusInternalServerError, "", err.Error())
return
}
rw.Header().Set("Content-Type", "application/jwt")
rw.Write([]byte(tokenString))
return
}
}
err = utils.WriteJSON(rw, http.StatusOK, responseAsMap, "")
if err != nil {
p.logger.WithError(err).Errorln("userinfo request failed writing response")
}
}
// EndSessionHandler implements the HTTP endpoint for RP initiated logout with
// OpenID Connect Session Management 1.0 as specified at
// https://openid.net/specs/openid-connect-session-1_0.html#RPLogout
func (p *Provider) EndSessionHandler(rw http.ResponseWriter, req *http.Request) {
var err error
var session *payload.Session
var currentIdentityManager identity.Manager
addResponseHeaders(rw.Header())
ctx := konnect.NewRequestContext(req.Context(), req)
// Validate request.
err = req.ParseForm()
if err != nil {
p.logger.WithError(err).Errorln("endsession request invalid form data")
p.ErrorPage(rw, http.StatusBadRequest, oidc.ErrorCodeOAuth2InvalidRequest, err.Error())
return
}
esr, err := payload.DecodeEndSessionRequest(req, p.metadata)
if err != nil {
p.logger.WithError(err).Errorln("endsession request invalid request data")
p.ErrorPage(rw, http.StatusBadRequest, oidc.ErrorCodeOAuth2InvalidRequest, err.Error())
return
}
err = esr.Validate(func(token *jwt.Token) (interface{}, error) {
// Validator for incoming IDToken hints, looks up key.
return p.validateJWT(token)
})
if err != nil {
goto done
}
// Get our session.
session, err = p.getSession(req)
if err != nil {
goto done
}
currentIdentityManager, err = p.getIdentityManagerFromSession(session)
if err != nil {
goto done
}
// Authorization unauthenticates end user.
err = currentIdentityManager.EndSession(ctx, rw, req, esr)
if err != nil {
goto done
}
done:
if err != nil {
switch err.(type) {
case *payload.AuthenticationBadRequest:
p.ErrorPage(rw, http.StatusBadRequest, err.Error(), err.(*payload.AuthenticationBadRequest).Description())
case *identity.RedirectError:
p.Found(rw, err.(*identity.RedirectError).RedirectURI(), nil, false)
case *identity.IsHandledError:
// do nothing
case *konnectoidc.OAuth2Error:
err = esr.NewError(err.Error(), err.(*konnectoidc.OAuth2Error).Description())
uri := esr.MakeRedirectEndSessionRequestURL()
if uri == nil {
p.ErrorPage(rw, http.StatusForbidden, err.Error(), "oauth2 error")
} else {
p.Found(rw, uri, err, false)
}
default:
p.logger.WithFields(utils.ErrorAsFields(err)).Errorln("endsession request failed")
p.ErrorPage(rw, http.StatusInternalServerError, err.Error(), "well sorry, but there was a problem")
}
return
}
// EndSession Response.
response := &payload.AuthenticationSuccess{
State: esr.State,
}
uri := esr.MakeRedirectEndSessionRequestURL()
if uri == nil {
err = utils.WriteJSON(rw, http.StatusOK, response, "")
if err != nil {
p.logger.WithError(err).Errorln("endsession request failed writing response")
}
} else {
p.Found(rw, uri, nil, false)
}
}
// CheckSessionIframeHandler implements the HTTP endpoint for OP iframe with
// OpenID Connect Session Management 1.0 as specified at
// https://openid.net/specs/openid-connect-session-1_0.html#OPiframe
func (p *Provider) CheckSessionIframeHandler(rw http.ResponseWriter, req *http.Request) {
addResponseHeaders(rw.Header())
nonce := rndm.GenerateRandomString(32)
rw.Header().Set("Content-Type", "text/html; charset=utf-8")
rw.Header().Set("X-XSS-Protection", "1; mode=block")
rw.Header().Set("Content-Security-Policy", fmt.Sprintf("default-src 'none'; script-src 'nonce-%s'", nonce))
data := struct {
CookieName string
Nonce string
}{
CookieName: p.browserStateCookieName,
Nonce: nonce,
}
checkSessionIframeTemplate.Execute(rw, data)
}
// RegistrationHandler implements the HTTP endpoint for client self registration
// with OpenID Connect Registration 1.0 as specified at
// https://openid.net/specs/openid-connect-registration-1_0.html#ClientRegistration
func (p *Provider) RegistrationHandler(rw http.ResponseWriter, req *http.Request) {
req.Body = http.MaxBytesReader(rw, req.Body, registrationSizeLimit)
addResponseHeaders(rw.Header())
crr, err := payload.DecodeClientRegistrationRequest(req)
if err != nil {
p.logger.WithError(err).Errorln("client registration request failed to decode request data")
p.ErrorPage(rw, http.StatusBadRequest, oidc.ErrorCodeOAuth2InvalidRequest, err.Error())
return
}
var cr *clients.ClientRegistration
// Validate request method
switch req.Method {
case http.MethodPost:
// breaks
default:
err = konnectoidc.NewOAuth2Error(oidc.ErrorCodeOAuth2InvalidRequest, "request must be sent with POST")
goto done
}
// Validate request.
err = crr.Validate()
if err != nil {
goto done
}
// Get registration record.
cr, err = crr.ClientRegistration()
if err != nil {
goto done
}
// Set client to dynamic. This creates the id and client secret.
err = cr.SetDynamic(clients.NewRegistryContext(req.Context(), p.clients), p.clients.StatelessCreator)
if err != nil {
goto done
}
done:
if err != nil {
switch err.(type) {
case *konnectoidc.OAuth2Error:
err = utils.WriteJSON(rw, http.StatusBadRequest, err, "")
if err != nil {
p.logger.WithError(err).Errorln("client registration request failed writing response")
return
}
default:
p.logger.WithFields(utils.ErrorAsFields(err)).Errorln("client registration request failed")
p.ErrorPage(rw, http.StatusInternalServerError, err.Error(), "well sorry, but there was a problem")
}
return
}
p.logger.WithFields(logrus.Fields{
"client_id": cr.ID,
"name": cr.Name,
"application_type": cr.ApplicationType,
"redirect_uris": cr.RedirectURIs,
}).Debugln("registered dynamic client")
response := &payload.ClientRegistrationResponse{
ClientID: cr.ID,
ClientSecret: cr.Secret,
ClientIDIssuedAt: cr.IDIssuedAt.Unix(),
ClientSecretExpiresAt: cr.SecretExpiresAt.Unix(),
ClientRegistrationRequest: *crr,
}
err = utils.WriteJSON(rw, http.StatusCreated, response, "")
if err != nil {
p.logger.WithError(err).Errorln("client registration request failed writing response")
}
}
+201
View File
@@ -0,0 +1,201 @@
/*
* 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 provider
import (
"html/template"
)
var checkSessionIframeTemplate = template.Must(template.New("check-session.html").Parse(`
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<script id="cookie-name" type="application/x-kkbs-name">{{.CookieName}}</script>
<script type="text/javascript" nonce={{.Nonce}}>
/*
Forge-SHA256 - https://github.com/brillout/forge-sha256/tree/6ad5535e0be2385fdc53f1d9ce2b172365c70333
The MIT License (MIT)
Copyright (c) 2015-2017 Romuald Brillout and contributors
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
Forge project - https://github.com/digitalbazaar/forge
New BSD License (3-clause)
Copyright (c) 2010, Digital Bazaar, Inc.
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.
* Neither the name of Digital Bazaar, Inc. nor the
names of its contributors may be used to endorse or promote products
derived from this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL DIGITAL BAZAAR BE LIABLE FOR ANY
DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
(function(){function p(a){this.data="";this.a=0;if("string"===typeof a)this.data=a;else if(b.D(a)||b.L(a)){a=new Uint8Array(a);try{this.data=String.fromCharCode.apply(null,a)}catch(f){for(var v=0;v<a.length;++v)this.M(a[v])}}else if(a instanceof p||"object"===typeof a&&"string"===typeof a.data&&"number"===typeof a.a)this.data=a.data,this.a=a.a;this.v=0}function w(a,f,b){for(var d,c,h,m,g,k,e,r,n,l,t,q,u,p=b.length();64<=p;){for(g=0;16>g;++g)f[g]=b.getInt32();for(;64>g;++g)d=f[g-2],d=(d>>>17|d<<15)^
(d>>>19|d<<13)^d>>>10,c=f[g-15],c=(c>>>7|c<<25)^(c>>>18|c<<14)^c>>>3,f[g]=d+f[g-7]+c+f[g-16]|0;k=a.g;e=a.h;r=a.i;n=a.j;l=a.l;t=a.m;q=a.o;u=a.s;for(g=0;64>g;++g)d=(l>>>6|l<<26)^(l>>>11|l<<21)^(l>>>25|l<<7),h=q^l&(t^q),c=(k>>>2|k<<30)^(k>>>13|k<<19)^(k>>>22|k<<10),m=k&e|r&(k^e),d=u+d+h+x[g]+f[g],c+=m,u=q,q=t,t=l,l=n+d|0,n=r,r=e,e=k,k=d+c|0;a.g=a.g+k|0;a.h=a.h+e|0;a.i=a.i+r|0;a.j=a.j+n|0;a.l=a.l+l|0;a.m=a.m+t|0;a.o=a.o+q|0;a.s=a.s+u|0;p-=64}}var m,y,e,b=m=m||{};b.D=function(a){return"undefined"!==typeof ArrayBuffer&&
a instanceof ArrayBuffer};b.L=function(a){return a&&b.D(a.buffer)&&void 0!==a.byteLength};b.G=p;b.b=p;b.b.prototype.H=function(a){this.v+=a;4096<this.v&&(this.v=0)};b.b.prototype.length=function(){return this.data.length-this.a};b.b.prototype.M=function(a){this.u(String.fromCharCode(a))};b.b.prototype.u=function(a){this.data+=a;this.H(a.length)};b.b.prototype.c=function(a){this.u(String.fromCharCode(a>>24&255)+String.fromCharCode(a>>16&255)+String.fromCharCode(a>>8&255)+String.fromCharCode(a&255))};
b.b.prototype.getInt16=function(){var a=this.data.charCodeAt(this.a)<<8^this.data.charCodeAt(this.a+1);this.a+=2;return a};b.b.prototype.getInt32=function(){var a=this.data.charCodeAt(this.a)<<24^this.data.charCodeAt(this.a+1)<<16^this.data.charCodeAt(this.a+2)<<8^this.data.charCodeAt(this.a+3);this.a+=4;return a};b.b.prototype.B=function(){return this.data.slice(this.a)};b.b.prototype.compact=function(){0<this.a&&(this.data=this.data.slice(this.a),this.a=0);return this};b.b.prototype.clear=function(){this.data=
"";this.a=0;return this};b.b.prototype.truncate=function(a){a=Math.max(0,this.length()-a);this.data=this.data.substr(this.a,a);this.a=0;return this};b.b.prototype.N=function(){for(var a="",f=this.a;f<this.data.length;++f){var b=this.data.charCodeAt(f);16>b&&(a+="0");a+=b.toString(16)}return a};b.b.prototype.toString=function(){return b.I(this.B())};b.createBuffer=function(a,f){void 0!==a&&"utf8"===(f||"raw")&&(a=b.C(a));return new b.G(a)};b.J=function(){for(var a=String.fromCharCode(0),b=64,e="";0<
b;)b&1&&(e+=a),b>>>=1,0<b&&(a+=a);return e};b.C=function(a){return unescape(encodeURIComponent(a))};b.I=function(a){return decodeURIComponent(escape(a))};b.K=function(a){for(var b=0;b<a.length;b++)if(a.charCodeAt(b)>>>8)return!0;return!1};var z=y=y||{};e=e||{};e.A=e.A||{};e.F=e.A.F=z;z.create=function(){A||(n=String.fromCharCode(128),n+=m.J(),x=[1116352408,1899447441,3049323471,3921009573,961987163,1508970993,2453635748,2870763221,3624381080,310598401,607225278,1426881987,1925078388,2162078206,2614888103,
3248222580,3835390401,4022224774,264347078,604807628,770255983,1249150122,1555081692,1996064986,2554220882,2821834349,2952996808,3210313671,3336571891,3584528711,113926993,338241895,666307205,773529912,1294757372,1396182291,1695183700,1986661051,2177026350,2456956037,2730485921,2820302411,3259730800,3345764771,3516065817,3600352804,4094571909,275423344,430227734,506948616,659060556,883997877,958139571,1322822218,1537002063,1747873779,1955562222,2024104815,2227730452,2361852424,2428436474,2756734187,
3204031479,3329325298],A=!0);var a=null,b=m.createBuffer(),e=Array(64),d={algorithm:"sha256",O:64,P:32,w:0,f:[0,0],start:function(){d.w=0;d.f=[0,0];b=m.createBuffer();a={g:1779033703,h:3144134277,i:1013904242,j:2773480762,l:1359893119,m:2600822924,o:528734635,s:1541459225};return d}};d.start();d.update=function(c,h){"utf8"===h&&(c=m.C(c));d.w+=c.length;d.f[0]+=c.length/4294967296>>>0;d.f[1]+=c.length>>>0;b.u(c);w(a,e,b);(2048<b.a||0===b.length())&&b.compact();return d};d.digest=function(){var c=m.createBuffer();
c.u(b.B());c.u(n.substr(0,64-(d.f[1]+8&63)));c.c(d.f[0]<<3|d.f[0]>>>28);c.c(d.f[1]<<3);var h={g:a.g,h:a.h,i:a.i,j:a.j,l:a.l,m:a.m,o:a.o,s:a.s};w(h,e,c);c=m.createBuffer();c.c(h.g);c.c(h.h);c.c(h.i);c.c(h.j);c.c(h.l);c.c(h.m);c.c(h.o);c.c(h.s);return c};return d};var n=null,A=!1,x=null;window.forge_sha256=function(a){var f=e.F.create();f.update(a,b.K(a)?"utf8":void 0);return f.digest().N()}})();
</script>
</head>
<body>
<noscript>
You need to enable JavaScript to run this app.
</noscript>
<script type="text/javascript" nonce={{.Nonce}}>
// This implements OpenID Connect Session Managament 1.0 OP IFrame as specified
// in https://openid.net/specs/openid-connect-session-1_0.html#OPiframe
(function() {
var cache = {};
// Get cookie name.
var cookieNameElem = document.getElementById('cookie-name');
if (cookieNameElem) {
var cookieName = cookieNameElem.textContent.trim();
}
// Helper to get cookie by name.
function getCookie(name) {
var value = '; ' + document.cookie;
var parts = value.split('; ' + name + "=");
if (parts.length === 2) {
return parts.pop().split(';').shift();
}
}
function getBrowserState() {
var browserState = getCookie(cookieName);
return browserState ? browserState : '';
}
function makeSessionState(clientID, origin, browserState, salt) {
return ''+forge_sha256(clientID + ' ' + origin + ' ' + browserState + ' ' + salt);
}
// Outer error catching data processor.
function receiveMessage(origin, data) {
if (!origin || !data) {
return 'error';
}
try {
return handleMessage(origin, data);
} catch (err) {
return 'error';
}
}
// Parse and validatie.
function handleMessage(origin, data) {
// Parse data.
var dataParts = data.split(' ');
if (dataParts.length !== 2) {
return 'error';
}
var clientID = dataParts[0];
var sessionState = dataParts[1];
if (!clientID || !sessionState) {
return 'error';
}
var sessionStateParts = sessionState.split('.');
if (sessionStateParts.length != 2) {
return 'error';
}
var clientStateHash = sessionStateParts[0];
var salt = sessionStateParts[1];
if (!clientStateHash || !salt) {
return 'error';
}
// Get browser state.
var browserState = getBrowserState();
// Make session state.
var expectedStateHash = makeSessionState(clientID, origin, browserState, salt);
// Compare.
var status = clientStateHash === expectedStateHash ? 'unchanged' : 'changed';
// Cache.
if (cache.browserState === browserState && status === 'changed' && cache.status === status) {
// If the browser state remains unchanged, something with the cookie
// did not work properly. To avoid a fast repeating loop, it is
// better to fail with error.
return 'error';
}
cache.status = status;
cache.browserState = browserState;
return status;
}
// Register event.
if (cookieName && window.parent !== window) {
window.addEventListener('message', function(event) {
// Only do something when receiving a message from our parent or
// from another window which shares our parent.
if (window.parent === event.source || (window !== event.source && window.parent === event.source.parent)) {
var response = receiveMessage(event.origin, event.data);
event.source.postMessage(response, event.origin);
}
}, false);
}
})();
</script>
</body>
</html>
`))
+61
View File
@@ -0,0 +1,61 @@
/*
* 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 provider
import (
"errors"
"github.com/golang-jwt/jwt/v5"
"github.com/libregraph/lico/identity"
"github.com/libregraph/lico/oidc/payload"
)
func (p *Provider) getIdentityManager(identityProvider string) (identity.Manager, error) {
if identityProvider == "" {
// Return default manager when empty (backwards compatibility).
return p.identityManager, nil
}
if identityProvider == p.identityManager.Name() {
return p.identityManager, nil
}
if p.guestManager != nil && identityProvider == p.guestManager.Name() {
return p.guestManager, nil
}
return nil, errors.New("identity provider mismatch")
}
func (p *Provider) getIdentityManagerFromClaims(identityProvider string, identityClaims jwt.MapClaims) (identity.Manager, error) {
if identityClaims == nil {
// Return default manager when no claims.
return p.identityManager, nil
}
return p.getIdentityManager(identityProvider)
}
func (p *Provider) getIdentityManagerFromSession(session *payload.Session) (identity.Manager, error) {
if session == nil {
// Return default manager when no session.
return p.identityManager, nil
}
return p.getIdentityManager(session.Provider)
}
+533
View File
@@ -0,0 +1,533 @@
/*
* 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 provider
import (
"context"
"crypto"
"crypto/ecdsa"
"crypto/rsa"
"crypto/x509"
"fmt"
"net/http"
"net/url"
"strings"
"time"
"github.com/golang-jwt/jwt/v5"
"github.com/libregraph/oidc-go"
"github.com/rs/cors"
"github.com/sirupsen/logrus"
"golang.org/x/crypto/ed25519"
konnect "github.com/libregraph/lico"
"github.com/libregraph/lico/identity"
"github.com/libregraph/lico/identity/clients"
identityManagers "github.com/libregraph/lico/identity/managers"
"github.com/libregraph/lico/managers"
konnectoidc "github.com/libregraph/lico/oidc"
"github.com/libregraph/lico/oidc/code"
"github.com/libregraph/lico/utils"
)
// Provider defines an OIDC provider with the handlers for the OIDC endpoints.
type Provider struct {
Config *Config
issuerIdentifier string
metadata *oidc.WellKnown
wellKnownPath string
jwksPath string
authorizationPath string
tokenPath string
userInfoPath string
endSessionPath string
checkSessionIframePath string
registrationPath string
identityManager identity.Manager
guestManager identity.Manager
codeManager code.Manager
encryptionManager *identityManagers.EncryptionManager
clients *clients.Registry
signingKeys map[jwt.SigningMethod]*SigningKey
signingMethodDefault jwt.SigningMethod
validationKeys map[string]crypto.PublicKey
certificates map[string][]*x509.Certificate
browserStateCookiePath string
browserStateCookieName string
browserStateCookieSameSite http.SameSite
sessionCookiePath string
sessionCookieName string
sessionCookieSameSite http.SameSite
accessTokenDuration time.Duration
idTokenDuration time.Duration
refreshTokenDuration time.Duration
logger logrus.FieldLogger
}
// NewProvider returns a new Provider.
func NewProvider(c *Config) (*Provider, error) {
p := &Provider{
Config: c,
issuerIdentifier: c.IssuerIdentifier,
wellKnownPath: c.WellKnownPath,
jwksPath: c.JwksPath,
authorizationPath: c.AuthorizationPath,
tokenPath: c.TokenPath,
userInfoPath: c.UserInfoPath,
endSessionPath: c.EndSessionPath,
checkSessionIframePath: c.CheckSessionIframePath,
registrationPath: c.RegistrationPath,
signingKeys: make(map[jwt.SigningMethod]*SigningKey),
validationKeys: make(map[string]crypto.PublicKey),
certificates: make(map[string][]*x509.Certificate),
browserStateCookiePath: c.BrowserStateCookiePath,
browserStateCookieName: c.BrowserStateCookieName,
browserStateCookieSameSite: c.BrowserStateCookieSameSite,
sessionCookiePath: c.SessionCookiePath,
sessionCookieName: c.SessionCookieName,
sessionCookieSameSite: c.SessionCookieSameSite,
accessTokenDuration: c.AccessTokenDuration,
idTokenDuration: c.IDTokenDuration,
refreshTokenDuration: c.RefreshTokenDuration,
logger: c.Config.Logger,
}
return p, nil
}
// RegisterManagers registers the provided managers from the
func (p *Provider) RegisterManagers(mgrs *managers.Managers) error {
p.identityManager = mgrs.Must("identity").(identity.Manager)
p.codeManager = mgrs.Must("code").(code.Manager)
p.encryptionManager = mgrs.Must("encryption").(*identityManagers.EncryptionManager)
p.clients = mgrs.Must("clients").(*clients.Registry)
// Register callback to cleanup our cookie whenever the identity is unset or
// set.
onSetLogon := func(ctx context.Context, rw http.ResponseWriter, user identity.User) error {
// NOTE(longsleep): This leaves room for optionmization. In theory it
// should be possible to set the new browser state here directly since
// the relevant information should be available in the identity manager
// and thus avoiding a potentially unset refresh and client side
// redirects whenever the same user signs in again.
return p.removeBrowserStateCookie(rw)
}
onUnsetLogon := func(ctx context.Context, rw http.ResponseWriter) error {
var err error
// Remove browser state cookie.
if errBsc := p.removeBrowserStateCookie(rw); errBsc != nil {
err = errBsc
}
// Remove OIDC client session cookie.
if errSc := p.removeSessionCookie(rw); errSc != nil {
err = errSc
}
return err
}
p.identityManager.OnSetLogon(onSetLogon)
p.identityManager.OnUnsetLogon(onUnsetLogon)
// Add guest manager if any can be found.
if guestManager, _ := mgrs.Get("guest"); guestManager != nil {
p.guestManager = guestManager.(identity.Manager)
p.guestManager.OnSetLogon(onSetLogon)
p.guestManager.OnUnsetLogon(onUnsetLogon)
}
if p.Config.RegistrationPath != "" {
// NOTE(longsleep): This is hackish. Find a better way to propagate our
// provides JWT stuff to the client registry.
p.clients.StatelessCreator = p.makeJWT
p.clients.StatelessValidator = p.validateJWT
}
return nil
}
func (p *Provider) makeIssURL(path string) string {
if path == "" {
return ""
}
u, _ := url.Parse(p.issuerIdentifier)
u.Path = "" // Strip path from issuer, whatever prefix must already be applied.
return fmt.Sprintf("%s%s", u.String(), path)
}
// SetSigningMethod sets the provided signing method as default signing method
// of the associated provider.
func (p *Provider) SetSigningMethod(signingMethod jwt.SigningMethod) error {
p.logger.WithField("alg", signingMethod.Alg()).Infoln("set provider signing alg")
p.signingMethodDefault = signingMethod
return nil
}
// SetSigningKey sets the provided signer as key for token signing with the
// provided id as key id. The public key of the provided signer is also added
// as validation key with the same key id.
func (p *Provider) SetSigningKey(id string, key crypto.Signer) error {
var signingMethod jwt.SigningMethod
// Auto select signingMethod based on the signer.
switch s := key.(type) {
case *rsa.PrivateKey:
signingMethod = jwt.SigningMethodPS256
case *ecdsa.PrivateKey:
signingMethod = jwt.SigningMethodES256
case ed25519.PrivateKey:
signingMethod = jwt.SigningMethodEdDSA
default:
return fmt.Errorf("unsupported signer type: %v", s)
}
if p.signingMethodDefault == nil {
if err := p.SetSigningMethod(signingMethod); err != nil {
return err
}
}
p.logger.WithFields(logrus.Fields{
"type": fmt.Sprintf("%T", key),
"id": id,
"method": fmt.Sprintf("%T", signingMethod),
}).Infoln("set provider signing key")
switch signingMethod.(type) {
case *jwt.SigningMethodECDSA:
// Add all other supported ECDSA signing methods as well.
p.signingKeys[jwt.SigningMethodES256] = &SigningKey{
ID: id,
PrivateKey: key,
SigningMethod: jwt.SigningMethodES256,
}
p.signingKeys[jwt.SigningMethodES384] = &SigningKey{
ID: id,
PrivateKey: key,
SigningMethod: jwt.SigningMethodES384,
}
p.signingKeys[jwt.SigningMethodES512] = &SigningKey{
ID: id,
PrivateKey: key,
SigningMethod: jwt.SigningMethodES512,
}
case *jwt.SigningMethodRSA:
// Add all supported RSA and RSAPSS signing methods as well.
p.signingKeys[jwt.SigningMethodRS256] = &SigningKey{
ID: id,
PrivateKey: key,
SigningMethod: jwt.SigningMethodRS256,
}
p.signingKeys[jwt.SigningMethodRS384] = &SigningKey{
ID: id,
PrivateKey: key,
SigningMethod: jwt.SigningMethodRS384,
}
p.signingKeys[jwt.SigningMethodRS512] = &SigningKey{
ID: id,
PrivateKey: key,
SigningMethod: jwt.SigningMethodRS512,
}
p.signingKeys[jwt.SigningMethodPS256] = &SigningKey{
ID: id,
PrivateKey: key,
SigningMethod: jwt.SigningMethodPS256,
}
p.signingKeys[jwt.SigningMethodPS384] = &SigningKey{
ID: id,
PrivateKey: key,
SigningMethod: jwt.SigningMethodPS384,
}
p.signingKeys[jwt.SigningMethodPS512] = &SigningKey{
ID: id,
PrivateKey: key,
SigningMethod: jwt.SigningMethodPS512,
}
case *jwt.SigningMethodRSAPSS:
// Add all supported RSA and RSAPSS signing methods as well.
p.signingKeys[jwt.SigningMethodRS256] = &SigningKey{
ID: id,
PrivateKey: key,
SigningMethod: jwt.SigningMethodRS256,
}
p.signingKeys[jwt.SigningMethodRS384] = &SigningKey{
ID: id,
PrivateKey: key,
SigningMethod: jwt.SigningMethodRS384,
}
p.signingKeys[jwt.SigningMethodRS512] = &SigningKey{
ID: id,
PrivateKey: key,
SigningMethod: jwt.SigningMethodRS512,
}
p.signingKeys[jwt.SigningMethodPS256] = &SigningKey{
ID: id,
PrivateKey: key,
SigningMethod: jwt.SigningMethodPS256,
}
p.signingKeys[jwt.SigningMethodPS384] = &SigningKey{
ID: id,
PrivateKey: key,
SigningMethod: jwt.SigningMethodPS384,
}
p.signingKeys[jwt.SigningMethodPS512] = &SigningKey{
ID: id,
PrivateKey: key,
SigningMethod: jwt.SigningMethodPS512,
}
case *jwt.SigningMethodEd25519:
p.signingKeys[signingMethod] = &SigningKey{
ID: id,
PrivateKey: key,
SigningMethod: signingMethod,
}
default:
return fmt.Errorf("unsupported signing method type")
}
if _, ok := p.signingKeys[signingMethod]; !ok {
return fmt.Errorf("unsupported signing method")
}
p.SetValidationKey(id, key.Public())
return nil
}
// GetSigningKey returns a matching signing key for the provided signing method.
func (p *Provider) GetSigningKey(signingMethod jwt.SigningMethod) (*SigningKey, bool) {
return p.getSigningKey(signingMethod)
}
func (p *Provider) getSigningKey(signingMethod jwt.SigningMethod) (*SigningKey, bool) {
if signingMethod == nil {
// Use default signign method if none given.
signingMethod = p.signingMethodDefault
}
sk, ok := p.signingKeys[signingMethod]
return sk, ok
}
// SetValidationKey sets the provider public key as validation key for token
// validation for tokens with the provided key.
func (p *Provider) SetValidationKey(id string, key crypto.PublicKey) error {
p.logger.WithFields(logrus.Fields{
"type": fmt.Sprintf("%T", key),
"id": id,
}).Infoln("set provider validation key")
p.validationKeys[id] = key
return nil
}
// GetValidationKey returns the validation key for the provided id.
func (p *Provider) GetValidationKey(id string) (crypto.PublicKey, bool) {
return p.getValidationKey(id)
}
func (p *Provider) getValidationKey(id string) (crypto.PublicKey, bool) {
vk, ok := p.validationKeys[id]
return vk, ok
}
// SetCertificate sets the provider certificate chain for a particular key.
func (p *Provider) SetCertificate(id string, certificates []*x509.Certificate) error {
p.logger.WithFields(logrus.Fields{
"chain_size": len(certificates),
"id": id,
}).Infoln("set provider certificate")
p.certificates[id] = certificates
return nil
}
// InitializeMetadata creates the accociated providers meta data document. Call
// this once all other settings at the provider have been done.
func (p *Provider) InitializeMetadata() error {
// Create well-known document.
p.metadata = &oidc.WellKnown{
Issuer: p.issuerIdentifier,
AuthorizationEndpoint: p.makeIssURL(p.authorizationPath),
TokenEndpoint: p.makeIssURL(p.tokenPath),
UserInfoEndpoint: p.makeIssURL(p.userInfoPath),
EndSessionEndpoint: p.makeIssURL(p.endSessionPath),
CheckSessionIframe: p.makeIssURL(p.checkSessionIframePath),
JwksURI: p.makeIssURL(p.jwksPath),
RegistrationEndpoint: p.makeIssURL(p.registrationPath),
ScopesSupported: uniqueStrings(append([]string{
oidc.ScopeOpenID,
}, p.identityManager.ScopesSupported(nil)...)),
ResponseTypesSupported: []string{
oidc.ResponseTypeIDTokenToken,
oidc.ResponseTypeIDToken,
oidc.ResponseTypeCodeIDToken,
oidc.ResponseTypeCodeIDTokenToken,
},
SubjectTypesSupported: []string{
oidc.SubjectIDPublic,
},
ClaimsParameterSupported: true,
ClaimsSupported: uniqueStrings(append([]string{
oidc.IssuerIdentifierClaim,
oidc.SubjectIdentifierClaim,
oidc.AudienceClaim,
oidc.ExpirationClaim,
oidc.IssuedAtClaim,
}, p.identityManager.ClaimsSupported(nil)...)),
RequestParameterSupported: true,
RequestURIParameterSupported: false,
}
p.metadata.IDTokenSigningAlgValuesSupported = make([]string, 0)
for alg := range p.signingKeys {
p.metadata.IDTokenSigningAlgValuesSupported = append(p.metadata.IDTokenSigningAlgValuesSupported, alg.Alg())
}
p.metadata.UserInfoSigningAlgValuesSupported = p.metadata.IDTokenSigningAlgValuesSupported
p.metadata.RequestObjectSigningAlgValuesSupported = []string{
jwt.SigningMethodES256.Alg(),
jwt.SigningMethodES384.Alg(),
jwt.SigningMethodES512.Alg(),
jwt.SigningMethodRS256.Alg(),
jwt.SigningMethodRS384.Alg(),
jwt.SigningMethodRS512.Alg(),
jwt.SigningMethodPS256.Alg(),
jwt.SigningMethodPS384.Alg(),
jwt.SigningMethodPS512.Alg(),
jwt.SigningMethodNone.Alg(),
jwt.SigningMethodEdDSA.Alg(),
}
p.metadata.TokenEndpointAuthMethodsSupported = []string{
oidc.AuthMethodClientSecretBasic,
oidc.AuthMethodNone,
}
p.metadata.TokenEndpointAuthSigningAlgValuesSupported = p.metadata.IDTokenSigningAlgValuesSupported
return nil
}
// ServerHTTP implements the http.HandlerFunc interface.
func (p *Provider) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
switch path := req.URL.Path; {
case path == p.wellKnownPath:
cors.Default().ServeHTTP(rw, req, p.WellKnownHandler)
case path == p.jwksPath:
cors.Default().ServeHTTP(rw, req, p.JwksHandler)
case path == p.authorizationPath:
p.AuthorizeHandler(rw, req)
case path == p.tokenPath:
cors.Default().ServeHTTP(rw, req, p.TokenHandler)
case path == p.userInfoPath:
// TODO(longsleep): Use more strict CORS.
cors.AllowAll().ServeHTTP(rw, req, p.UserInfoHandler)
case path == p.endSessionPath:
p.EndSessionHandler(rw, req)
case path == p.checkSessionIframePath:
p.CheckSessionIframeHandler(rw, req)
case path == p.registrationPath:
p.RegistrationHandler(rw, req)
default:
http.NotFound(rw, req)
}
}
// ErrorPage writes a HTML error page to the provided ResponseWriter.
func (p *Provider) ErrorPage(rw http.ResponseWriter, code int, title string, message string) {
utils.WriteErrorPage(rw, code, title, message)
}
// Found writes a HTTP 302 to the provided ResponseWriter with the appropriate
// Location header creates from the other parameters.
func (p *Provider) Found(rw http.ResponseWriter, uri *url.URL, params interface{}, asFragment bool) {
err := utils.WriteRedirect(rw, http.StatusFound, uri, params, asFragment)
if err != nil {
p.logger.WithError(err).Debugln("failed to write to response")
p.ErrorPage(rw, http.StatusInternalServerError, "", err.Error())
return
}
}
// LoginRequiredPage writes a HTTP 30 to the provided ResponseWrite with the
// URL of the provided request (set to the scheme and host of issuer) as
// continue parameter.
func (p *Provider) LoginRequiredPage(rw http.ResponseWriter, req *http.Request, uri *url.URL) {
issURI, _ := url.Parse(p.issuerIdentifier)
trusted, _ := utils.IsRequestFromTrustedSource(req, p.Config.Config.TrustedProxyIPs, p.Config.Config.TrustedProxyNets)
continueURI := getRequestURL(req, trusted)
continueURI.Scheme = issURI.Scheme
continueURI.Host = issURI.Host
uri, err := url.Parse(fmt.Sprintf("%s?continue=%s&oauth=1", uri.String(), url.QueryEscape(continueURI.String())))
if err != nil {
p.logger.WithError(err).Debugln("failed to parse sign-in URL")
p.ErrorPage(rw, http.StatusInternalServerError, "", err.Error())
return
}
p.Found(rw, uri, nil, false)
}
// GetAccessTokenClaimsFromRequest reads incoming request, validates the
// access token and returns the validated claims.
func (p *Provider) GetAccessTokenClaimsFromRequest(req *http.Request) (*konnect.AccessTokenClaims, error) {
var err error
var claims *konnect.AccessTokenClaims
auth := strings.SplitN(req.Header.Get("Authorization"), " ", 2)
switch auth[0] {
case oidc.TokenTypeBearer:
if len(auth) != 2 {
err = konnectoidc.NewOAuth2Error(oidc.ErrorCodeOAuth2InvalidRequest, "Invalid Bearer authorization header format")
break
}
claims = &konnect.AccessTokenClaims{}
_, err = jwt.ParseWithClaims(auth[1], claims, func(token *jwt.Token) (interface{}, error) {
// Validator for incoming access tokens, looks up key.
return p.validateJWT(token)
})
if err != nil {
// Wrap as OAuth2 error.
err = konnectoidc.NewOAuth2Error(oidc.ErrorCodeOAuth2InvalidToken, err.Error())
}
default:
err = konnectoidc.NewOAuth2Error(oidc.ErrorCodeOAuth2InvalidRequest, "Bearer authorization required")
}
return claims, err
}
+145
View File
@@ -0,0 +1,145 @@
/*
* 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 provider
import (
"bytes"
"encoding/base64"
"encoding/gob"
"net/http"
"github.com/golang-jwt/jwt/v5"
"github.com/longsleep/rndm"
konnect "github.com/libregraph/lico"
"github.com/libregraph/lico/identity"
"github.com/libregraph/lico/oidc"
"github.com/libregraph/lico/oidc/payload"
)
const sessionVersion = 2
func (p *Provider) getSession(req *http.Request) (*payload.Session, error) {
serialized, err := p.getSessionCookie(req)
switch err {
case nil:
// breaks
case http.ErrNoCookie:
return nil, nil
default:
return nil, err
}
// Decode.
return p.unserializeSession(serialized)
}
func (p *Provider) updateOrCreateSession(rw http.ResponseWriter, req *http.Request, ar *payload.AuthenticationRequest, auth identity.AuthRecord) (*payload.Session, error) {
session := ar.Session
if session != nil && session.Version == sessionVersion && session.Sub == auth.Subject() {
// Existing session with same sub.
return session, nil
}
// Create new session.
session = &payload.Session{
Version: sessionVersion,
ID: rndm.GenerateRandomString(32),
Sub: auth.Subject(),
Provider: auth.Manager().Name(),
}
serialized, err := p.serializeSession(session)
if err != nil {
return session, err
}
err = p.setSessionCookie(rw, serialized)
return session, err
}
func (p *Provider) serializeSession(session *payload.Session) (string, error) {
var b bytes.Buffer
enc := gob.NewEncoder(&b)
err := enc.Encode(session)
if err != nil {
return "", err
}
ciphertext, err := p.encryptionManager.Encrypt(b.Bytes())
if err != nil {
return "", err
}
return base64.StdEncoding.EncodeToString(ciphertext), nil
}
func (p *Provider) unserializeSession(value string) (*payload.Session, error) {
ciphertext, err := base64.StdEncoding.DecodeString(value)
if err != nil {
return nil, err
}
raw, err := p.encryptionManager.Decrypt(ciphertext)
if err != nil {
return nil, err
}
var session payload.Session
r := bytes.NewReader(raw)
dec := gob.NewDecoder(r)
err = dec.Decode(&session)
if err != nil {
return nil, err
}
return &session, nil
}
func (p *Provider) getUserIDAndSessionRefFromClaims(claims *jwt.RegisteredClaims, sessionClaims *oidc.SessionClaims, identityClaims jwt.MapClaims) (string, *string) {
if claims == nil || identityClaims == nil {
return "", nil
}
if len(claims.Audience) != 1 {
return "", nil
}
audience := claims.Audience[0]
userIDClaim, _ := identityClaims[konnect.IdentifiedUserIDClaim].(string)
if userIDClaim == "" {
return userIDClaim, nil
}
userClaim, _ := identityClaims[konnect.IdentifiedUserClaim].(string)
if userClaim == "" {
userClaim = userIDClaim
}
if sessionClaims != nil {
sessionIDClaim := sessionClaims.SessionID
if sessionIDClaim != "" {
return userIDClaim, &sessionIDClaim
}
}
// NOTE(longsleep): Return the userID from claims and generate a session ref
// for it. Session refs use the userClaim if available and set by the
// underlaying backend.
return userIDClaim, identity.GetSessionRef(p.identityManager.Name(), audience, userClaim)
}
+31
View File
@@ -0,0 +1,31 @@
/*
* 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 provider
import (
"crypto"
"github.com/golang-jwt/jwt/v5"
)
// A SigningKey bundles a signer with meta data and a signign method.
type SigningKey struct {
ID string
PrivateKey crypto.Signer
SigningMethod jwt.SigningMethod
}
+105
View File
@@ -0,0 +1,105 @@
/*
* 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 provider
import (
"crypto/sha256"
"encoding/base64"
"encoding/hex"
"fmt"
"net/http"
"net/url"
"github.com/longsleep/rndm"
"golang.org/x/crypto/blake2b"
"github.com/libregraph/lico/identity"
"github.com/libregraph/lico/oidc/payload"
)
var browserStateMarker = []byte("kopano-konnect-1")
func (p *Provider) makeBrowserState(ar *payload.AuthenticationRequest, auth identity.AuthRecord, err error) (string, error) {
hasher, hasherErr := blake2b.New256(nil)
if hasherErr != nil {
return "", hasherErr
}
if auth != nil && err == nil {
hasher.Write([]byte(auth.Subject()))
} else {
// Use empty string value when not signed in or with error. This means
// that a browser state is always created.
hasher.Write([]byte(" "))
}
hasher.Write([]byte(" "))
hasher.Write([]byte(p.issuerIdentifier))
hasher.Write([]byte(" "))
hasher.Write(browserStateMarker)
// Encode to string.
browserState := base64.RawURLEncoding.EncodeToString(hasher.Sum(nil))
return browserState, nil
}
func (p *Provider) makeSessionState(req *http.Request, ar *payload.AuthenticationRequest, browserState string) (string, error) {
var origin string
for {
redirectURL := ar.RedirectURI
if redirectURL != nil {
origin = fmt.Sprintf("%s://%s", redirectURL.Scheme, redirectURL.Host)
break
}
originHeader := req.Header.Get("Origin")
if originHeader != "" {
origin = originHeader
break
}
refererHeader := req.Header.Get("Referer")
if refererHeader != "" {
// Rescure from referer.
refererURL, err := url.Parse(refererHeader)
if err != nil {
return "", fmt.Errorf("invalid referer value: %v", err)
}
origin = fmt.Sprintf("%s://%s", refererURL.Scheme, refererURL.Host)
break
}
return "", fmt.Errorf("missing origin")
}
salt := rndm.GenerateRandomString(32)
hasher := sha256.New()
hasher.Write([]byte(ar.ClientID))
hasher.Write([]byte(" "))
hasher.Write([]byte(origin))
hasher.Write([]byte(" "))
hasher.Write([]byte(browserState))
hasher.Write([]byte(" "))
hasher.Write([]byte(salt))
sessionState := fmt.Sprintf("%s.%s", hex.EncodeToString(hasher.Sum(nil)), salt)
return sessionState, nil
}
+43
View File
@@ -0,0 +1,43 @@
/*
* 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 provider
import (
"errors"
konnect "github.com/libregraph/lico"
"github.com/libregraph/lico/identity"
)
// PublicSubjectFromAuth creates the provideds auth Subject value with the
// accociated provider. This subject can be used as URL safe value to uniquely
// identify the provided auth user with remote systems.
func (p *Provider) PublicSubjectFromAuth(auth identity.AuthRecord) (string, error) {
authorizedScopes := auth.AuthorizedScopes()
if ok, _ := authorizedScopes[konnect.ScopeRawSubject]; ok {
// Return raw subject as is when with ScopeRawSubject.
user := auth.User()
if user == nil {
return "", errors.New("no user")
}
return user.Raw(), nil
}
return auth.Subject(), nil
}
+395
View File
@@ -0,0 +1,395 @@
/*
* Copyright 2017-2021 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 provider
import (
"context"
"fmt"
"time"
"github.com/golang-jwt/jwt/v5"
"github.com/libregraph/oidc-go"
"github.com/longsleep/rndm"
konnect "github.com/libregraph/lico"
"github.com/libregraph/lico/identity"
konnectoidc "github.com/libregraph/lico/oidc"
"github.com/libregraph/lico/oidc/payload"
"github.com/libregraph/lico/utils"
)
// MakeAccessToken implements the oidc.AccessTokenProvider interface.
func (p *Provider) MakeAccessToken(ctx context.Context, audience string, auth identity.AuthRecord) (string, error) {
return p.makeAccessToken(ctx, audience, auth, nil, nil)
}
func (p *Provider) makeAccessToken(ctx context.Context, audience string, auth identity.AuthRecord, signingMethod jwt.SigningMethod, refreshTokenClaims *konnect.RefreshTokenClaims) (string, error) {
sk, ok := p.getSigningKey(signingMethod)
if !ok {
return "", fmt.Errorf("no signing key")
}
authorizedScopes := auth.AuthorizedScopes()
authorizedScopesList := payload.ScopesValue(makeArrayFromBoolMap(authorizedScopes))
accessTokenClaims := konnect.AccessTokenClaims{
TokenType: konnect.TokenTypeAccessToken,
AuthorizedScopesList: authorizedScopesList,
AuthorizedClaimsRequest: auth.AuthorizedClaims(),
RegisteredClaims: jwt.RegisteredClaims{
Issuer: p.issuerIdentifier,
Subject: auth.Subject(),
Audience: jwt.ClaimStrings{audience},
ExpiresAt: jwt.NewNumericDate(time.Now().Add(p.accessTokenDuration)),
IssuedAt: jwt.NewNumericDate(time.Now()),
ID: rndm.GenerateRandomString(24),
},
}
user := auth.User()
if user != nil {
if userWithClaims, ok := user.(identity.UserWithClaims); ok {
accessTokenClaims.IdentityClaims = userWithClaims.Claims()
}
accessTokenClaims.IdentityProvider = auth.Manager().Name()
if accessTokenClaims.IdentityClaims != nil && refreshTokenClaims != nil && refreshTokenClaims.IdentityClaims != nil {
if refreshTokenClaims.IdentityProvider != accessTokenClaims.IdentityProvider {
return "", fmt.Errorf("refresh token claims provider mismatch")
}
for k, v := range refreshTokenClaims.IdentityClaims {
// Force to use refresh token identity claim values. This also locks all
// the extra claims for id and access tokens to the ones provided from
// the refresh token claims (which currently includes the session id).
accessTokenClaims.IdentityClaims[k] = v
}
}
}
// Support additional custom user specific claims.
var finalAccessTokenClaims jwt.Claims = accessTokenClaims
if accessTokenClaims.IdentityClaims != nil {
accessTokenClaimsMap, err := payload.ToMap(accessTokenClaims)
if err != nil {
return "", err
}
delete(accessTokenClaimsMap[konnect.IdentityClaim].(map[string]interface{}), konnect.InternalExtraIDTokenClaimsClaim)
delete(accessTokenClaimsMap[konnect.IdentityClaim].(map[string]interface{}), konnect.InternalExtraAccessTokenClaimsClaim)
// Look for special internal key, if its a map all claims in there are
// elevated to top level.
extraClaimsMap, _ := accessTokenClaims.IdentityClaims[konnect.InternalExtraAccessTokenClaimsClaim].(map[string]interface{})
if extraClaimsMap != nil {
// Inject extra claims.
for claim, value := range extraClaimsMap {
switch claim {
case konnect.ScopesClaim:
// Support to extend the scopes.
extraScopesList, _ := value.(string)
if extraScopesList != "" {
authorizedScopesList = append(authorizedScopesList, extraScopesList)
value = authorizedScopesList
}
default:
if _, ok := accessTokenClaimsMap[claim]; ok {
// Prevent override of existing claims, only allow new claims.
continue
}
}
accessTokenClaimsMap[claim] = value
}
}
finalAccessTokenClaims = jwt.MapClaims(accessTokenClaimsMap)
}
accessToken := jwt.NewWithClaims(sk.SigningMethod, finalAccessTokenClaims)
accessToken.Header[oidc.JWTHeaderKeyID] = sk.ID
return accessToken.SignedString(sk.PrivateKey)
}
func (p *Provider) makeIDToken(ctx context.Context, ar *payload.AuthenticationRequest, auth identity.AuthRecord, session *payload.Session, accessTokenString string, codeString string, signingMethod jwt.SigningMethod, refreshTokenClaims *konnect.RefreshTokenClaims) (string, error) {
sk, ok := p.getSigningKey(signingMethod)
if !ok {
return "", fmt.Errorf("no signing key")
}
publicSubject, err := p.PublicSubjectFromAuth(auth)
if err != nil {
return "", err
}
idTokenClaims := &konnectoidc.IDTokenClaims{
Nonce: ar.Nonce,
RegisteredClaims: jwt.RegisteredClaims{
Issuer: p.issuerIdentifier,
Subject: publicSubject,
Audience: jwt.ClaimStrings{ar.ClientID},
ExpiresAt: jwt.NewNumericDate(time.Now().Add(p.idTokenDuration)),
IssuedAt: jwt.NewNumericDate(time.Now()),
},
}
accessTokenClaims := konnect.AccessTokenClaims{}
if session != nil {
// Include session data in ID token.
idTokenClaims.SessionClaims = &konnectoidc.SessionClaims{
SessionID: session.ID,
}
}
// Include requested scope data in ID token when no access token is
// generated.
authorizedClaimsRequest := auth.AuthorizedClaims()
withAccessToken := accessTokenString != ""
withCode := codeString != ""
withAuthTime := ar.MaxAge > 0
withIDTokenClaimsRequest := authorizedClaimsRequest != nil && authorizedClaimsRequest.IDToken != nil
user := auth.User()
if user == nil {
return "", fmt.Errorf("no user")
}
if userWithClaims, ok := user.(identity.UserWithClaims); ok {
accessTokenClaims.IdentityClaims = userWithClaims.Claims()
}
accessTokenClaims.IdentityProvider = auth.Manager().Name()
if accessTokenClaims.IdentityClaims != nil && refreshTokenClaims != nil && refreshTokenClaims.IdentityClaims != nil {
if refreshTokenClaims.IdentityProvider != accessTokenClaims.IdentityProvider {
return "", fmt.Errorf("refresh token claims provider mismatch")
}
for k, v := range refreshTokenClaims.IdentityClaims {
// Force to use refresh token identity claim values. This also locks all
// the extra claims for id and access tokens to the ones provided from
// the refresh token claims (which currently includes the session id).
accessTokenClaims.IdentityClaims[k] = v
}
}
if withIDTokenClaimsRequest {
// Apply additional information from ID token claims request.
if _, ok := authorizedClaimsRequest.IDToken.Get(oidc.AuthTimeClaim); !withAuthTime && ok {
// Return auth time claim if requested and not already requested by other means.
withAuthTime = true
}
}
if !withAccessToken || withIDTokenClaimsRequest {
var userID string
if accessTokenClaims.IdentityClaims != nil {
if userIDString, ok := accessTokenClaims.IdentityClaims[konnect.IdentifiedUserIDClaim]; ok {
userID = userIDString.(string)
}
}
if userID == "" {
return "", fmt.Errorf("no id claim in user identity claims")
}
var sessionRef *string
if userWithSessionRef, ok := user.(identity.UserWithSessionRef); ok {
sessionRef = userWithSessionRef.SessionRef()
}
var requestedClaimsMap []*payload.ClaimsRequestMap
var requestedScopesMap map[string]bool
if withIDTokenClaimsRequest {
requestedClaimsMap = []*payload.ClaimsRequestMap{authorizedClaimsRequest.IDToken}
requestedScopesMap = authorizedClaimsRequest.IDToken.ScopesMap(nil)
}
authorizedScopes := auth.AuthorizedScopes()
freshAuth, found, fetchErr := auth.Manager().Fetch(ctx, userID, sessionRef, authorizedScopes, requestedClaimsMap, authorizedScopes)
if fetchErr != nil {
p.logger.WithFields(utils.ErrorAsFields(fetchErr)).Errorln("identity manager fetch failed")
found = false
}
if !found {
return "", fmt.Errorf("user not found")
}
if (!withAccessToken && ar.Scopes[oidc.ScopeProfile]) || requestedScopesMap[oidc.ScopeProfile] {
idTokenClaims.ProfileClaims = konnectoidc.NewProfileClaims(freshAuth.Claims(oidc.ScopeProfile)[0])
}
if (!withAccessToken && ar.Scopes[oidc.ScopeEmail]) || requestedScopesMap[oidc.ScopeEmail] {
idTokenClaims.EmailClaims = konnectoidc.NewEmailClaims(freshAuth.Claims(oidc.ScopeEmail)[0])
}
auth = freshAuth
}
if withAccessToken {
// Add left-most hash of access token.
// http://openid.net/specs/openid-connect-core-1_0.html#ImplicitIDToken
hash, hashErr := oidc.HashFromSigningMethod(sk.SigningMethod.Alg())
if hashErr != nil {
return "", hashErr
}
idTokenClaims.AccessTokenHash = oidc.LeftmostHash([]byte(accessTokenString), hash).String()
}
if withCode {
// Add left-most hash of code.
// http://openid.net/specs/openid-connect-core-1_0.html#HybridIDToken
hash, hashErr := oidc.HashFromSigningMethod(sk.SigningMethod.Alg())
if hashErr != nil {
return "", hashErr
}
idTokenClaims.CodeHash = oidc.LeftmostHash([]byte(codeString), hash).String()
}
if withAuthTime {
// Add AuthTime.
if loggedOn, logonAt := auth.LoggedOn(); loggedOn {
idTokenClaims.AuthTime = logonAt.Unix()
} else {
// NOTE(longsleep): Return current time to be spec compliant.
idTokenClaims.AuthTime = time.Now().Unix()
}
}
// To support extra non-standard claims in ID token, convert claim set to
// map.
idTokenClaimsMap, err := payload.ToMap(idTokenClaims)
if err != nil {
return "", err
}
if accessTokenClaims.IdentityClaims != nil {
// Inject available extra ID token claims.
extraClaimsMap, _ := accessTokenClaims.IdentityClaims[konnect.InternalExtraIDTokenClaimsClaim].(map[string]interface{})
if extraClaimsMap != nil {
for claim, value := range extraClaimsMap {
idTokenClaimsMap[claim] = value
}
}
}
if !withAccessToken && accessTokenClaims.IdentityClaims != nil {
// Include requested scope data in ID token when no access token is
// generated - additional custom user specific claims.
// Inject extra claims.
extraClaimsMap, _ := accessTokenClaims.IdentityClaims[konnect.InternalExtraAccessTokenClaimsClaim].(map[string]interface{})
if extraClaimsMap != nil {
for claim, value := range extraClaimsMap {
idTokenClaimsMap[claim] = value
}
}
}
// Create signed token.
idToken := jwt.NewWithClaims(sk.SigningMethod, jwt.MapClaims(idTokenClaimsMap))
idToken.Header[oidc.JWTHeaderKeyID] = sk.ID
return idToken.SignedString(sk.PrivateKey)
}
func (p *Provider) makeRefreshToken(ctx context.Context, audience string, auth identity.AuthRecord, signingMethod jwt.SigningMethod) (string, error) {
sk, ok := p.getSigningKey(signingMethod)
if !ok {
return "", fmt.Errorf("no signing key")
}
approvedScopesList := []string{}
approvedScopes := make(map[string]bool)
for scope, granted := range auth.AuthorizedScopes() {
if granted {
approvedScopesList = append(approvedScopesList, scope)
approvedScopes[scope] = true
}
}
ref, err := auth.Manager().ApproveScopes(ctx, auth.Subject(), audience, approvedScopes)
if err != nil {
return "", err
}
refreshTokenClaims := &konnect.RefreshTokenClaims{
TokenType: konnect.TokenTypeRefreshToken,
ApprovedScopesList: approvedScopesList,
ApprovedClaimsRequest: auth.AuthorizedClaims(),
Ref: ref,
RegisteredClaims: jwt.RegisteredClaims{
Issuer: p.issuerIdentifier,
Subject: auth.Subject(),
Audience: jwt.ClaimStrings{audience},
ExpiresAt: jwt.NewNumericDate(time.Now().Add(p.refreshTokenDuration)),
IssuedAt: jwt.NewNumericDate(time.Now()),
ID: rndm.GenerateRandomString(24),
},
}
user := auth.User()
if user != nil {
if userWithClaims, ok := user.(identity.UserWithClaims); ok {
refreshTokenClaims.IdentityClaims = userWithClaims.Claims()
}
refreshTokenClaims.IdentityProvider = auth.Manager().Name()
}
refreshToken := jwt.NewWithClaims(sk.SigningMethod, refreshTokenClaims)
refreshToken.Header[oidc.JWTHeaderKeyID] = sk.ID
return refreshToken.SignedString(sk.PrivateKey)
}
func (p *Provider) makeJWT(ctx context.Context, signingMethod jwt.SigningMethod, claims jwt.Claims) (string, error) {
sk, ok := p.getSigningKey(signingMethod)
if !ok {
return "", fmt.Errorf("no signing key")
}
token := jwt.NewWithClaims(sk.SigningMethod, claims)
token.Header[oidc.JWTHeaderKeyID] = sk.ID
return token.SignedString(sk.PrivateKey)
}
func (p *Provider) validateJWT(token *jwt.Token) (interface{}, error) {
rawAlg, ok := token.Header[oidc.JWTHeaderAlg]
if !ok {
return nil, fmt.Errorf("No alg header")
}
alg, ok := rawAlg.(string)
if !ok {
return nil, fmt.Errorf("Invalid alg value")
}
switch jwt.GetSigningMethod(alg).(type) {
case *jwt.SigningMethodRSA:
case *jwt.SigningMethodECDSA:
case *jwt.SigningMethodRSAPSS:
default:
return nil, fmt.Errorf("Unexpected alg value")
}
rawKid, ok := token.Header[oidc.JWTHeaderKeyID]
if !ok {
return nil, fmt.Errorf("No kid header")
}
kid, ok := rawKid.(string)
if !ok {
return nil, fmt.Errorf("Invalid kid value")
}
key, ok := p.getValidationKey(kid)
if !ok {
return nil, fmt.Errorf("Unknown kid")
}
return key, nil
}
+85
View File
@@ -0,0 +1,85 @@
/*
* 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 provider
import (
"fmt"
"net/http"
"net/url"
"time"
)
var (
farPastExpiryTime = time.Unix(0, 0)
)
func uniqueStrings(s []string) []string {
var res []string
m := make(map[string]bool)
for _, s := range s {
if _, ok := m[s]; ok {
continue
}
m[s] = true
res = append(res, s)
}
return res
}
func getRequestURL(req *http.Request, isTrustedSource bool) *url.URL {
u, _ := url.Parse(req.URL.String())
if isTrustedSource {
// Look at proxy injected values to rewrite URLs if trusted.
for {
prefix := req.Header.Get("X-Forwarded-Prefix")
if prefix != "" {
u.Path = fmt.Sprintf("%s%s", prefix, u.Path)
break
}
replaced := req.Header.Get("X-Replaced-Path")
if replaced != "" {
u.Path = replaced
break
}
break
}
}
return u
}
func addResponseHeaders(header http.Header) {
header.Set("Cache-Control", "no-cache, no-store, must-revalidate")
header.Set("Pragma", "no-cache")
header.Set("X-Content-Type-Options", "nosniff")
header.Set("Referrer-Policy", "origin")
}
func makeArrayFromBoolMap(m map[string]bool) []string {
result := []string{}
for k, v := range m {
if v {
result = append(result, k)
}
}
return result
}