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