Initial QSfera import

This commit is contained in:
Курнат Андрей
2026-06-07 10:20:04 +03:00
commit 2315f25754
16485 changed files with 4826827 additions and 0 deletions
+462
View File
@@ -0,0 +1,462 @@
/*
* Copyright 2017-2019 Kopano and its licensors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package payload
import (
"fmt"
"net/http"
"net/url"
"strconv"
"strings"
"time"
"github.com/golang-jwt/jwt/v5"
"github.com/libregraph/oidc-go"
konnectoidc "github.com/libregraph/lico/oidc"
)
// AuthenticationRequest holds the incoming parameters and request data for
// the OpenID Connect 1.0 authorization endpoint as specified at
// http://openid.net/specs/openid-connect-core-1_0.html#AuthRequest and
// http://openid.net/specs/openid-connect-core-1_0.html#ImplicitAuthRequest
type AuthenticationRequest struct {
providerMetadata *oidc.WellKnown
RawScope string `schema:"scope"`
Claims *ClaimsRequest `schema:"claims"`
RawResponseType string `schema:"response_type"`
ResponseMode string `schema:"response_mode"`
ClientID string `schema:"client_id"`
RawRedirectURI string `schema:"redirect_uri"`
State string `schema:"state"`
Nonce string `schema:"nonce"`
RawPrompt string `schema:"prompt"`
RawIDTokenHint string `schema:"id_token_hint"`
RawMaxAge string `schema:"max_age"`
RawRequest string `schema:"request"`
RawRequestURI string `schema:"request_uri"`
RawRegistration string `schema:"registration"`
CodeChallenge string `schema:"code_challenge"`
CodeChallengeMethod string `schema:"code_challenge_method"`
Scopes map[string]bool `schema:"-"`
ResponseTypes map[string]bool `schema:"-"`
Prompts map[string]bool `schema:"-"`
RedirectURI *url.URL `schema:"-"`
IDTokenHint *jwt.Token `schema:"-"`
MaxAge time.Duration `schema:"-"`
Request *jwt.Token `schema:"-"`
UseFragment bool `schema:"-"`
Flow string `schema:"-"`
Session *Session `schema:"-"`
}
// DecodeAuthenticationRequest returns a AuthenticationRequest holding the
// provided requests form data.
func DecodeAuthenticationRequest(req *http.Request, providerMetadata *oidc.WellKnown, keyFunc jwt.Keyfunc) (*AuthenticationRequest, error) {
return NewAuthenticationRequest(req.Form, providerMetadata, keyFunc)
}
// NewAuthenticationRequest returns a AuthenticationRequest holding the
// provided url values.
func NewAuthenticationRequest(values url.Values, providerMetadata *oidc.WellKnown, keyFunc jwt.Keyfunc) (*AuthenticationRequest, error) {
ar := &AuthenticationRequest{
providerMetadata: providerMetadata,
Scopes: make(map[string]bool),
ResponseTypes: make(map[string]bool),
Prompts: make(map[string]bool),
}
err := DecodeSchema(ar, values)
if err != nil {
return nil, fmt.Errorf("failed to decode authentication request: %v", err)
}
if ar.RawScope != "" {
// Parse scope early, since the value is needed to handle the request
// parameter properly.
for _, scope := range strings.Split(ar.RawScope, " ") {
ar.Scopes[scope] = true
}
}
if ar.RawRequest != "" {
parser := &jwt.Parser{}
request, err := parser.ParseWithClaims(ar.RawRequest, &RequestObjectClaims{}, func(token *jwt.Token) (interface{}, error) {
if keyFunc != nil {
return keyFunc(token)
}
return nil, fmt.Errorf("Not validated")
})
if err != nil {
return nil, ar.NewBadRequest(oidc.ErrorCodeOIDCInvalidRequestObject, err.Error())
}
if claims, ok := request.Claims.(*RequestObjectClaims); ok {
err = ar.ApplyRequestObject(claims, request.Method)
if err != nil {
return nil, err
}
}
ar.Request = request
}
ar.RedirectURI, _ = url.Parse(ar.RawRedirectURI)
if ar.RawResponseType != "" {
for _, rt := range strings.Split(ar.RawResponseType, " ") {
ar.ResponseTypes[rt] = true
}
}
if ar.RawPrompt != "" {
for _, prompt := range strings.Split(ar.RawPrompt, " ") {
ar.Prompts[prompt] = true
}
}
switch ar.RawResponseType {
case oidc.ResponseTypeCode:
// Code flow.
ar.Flow = oidc.FlowCode
// breaks
case oidc.ResponseTypeIDToken:
// Implicit flow.
fallthrough
case oidc.ResponseTypeIDTokenToken:
// Implicit flow with access token.
ar.UseFragment = true
ar.Flow = oidc.FlowImplicit
case oidc.ResponseTypeCodeIDToken:
// Hybrid flow.
fallthrough
case oidc.ResponseTypeCodeToken:
// Hybgrid flow.
fallthrough
case oidc.ResponseTypeCodeIDTokenToken:
// Hybrid flow.
ar.UseFragment = true
ar.Flow = oidc.FlowHybrid
}
switch ar.ResponseMode {
case oidc.ResponseModeFragment:
ar.UseFragment = true
// breaks
case oidc.ResponseModeQuery:
ar.UseFragment = false
// breaks
}
if ar.RawMaxAge != "" {
maxAgeInt, err := strconv.ParseInt(ar.RawMaxAge, 10, 64)
if err != nil {
return nil, err
}
ar.MaxAge = time.Duration(maxAgeInt) * time.Second
}
if ar.Claims != nil && ar.Claims.Passthru != nil {
// Remove pass thru claims when not provided in a secure manner. This
// means that pass through claims can only be passed via a signed request
// objects and its claims.
if ar.Request == nil || ar.Request.Method == jwt.SigningMethodNone || ar.Request.Claims == nil {
ar.Claims.Passthru = nil
}
}
return ar, nil
}
// ApplyRequestObject applies the provided request object claims to the
// associated authentication request data with validation as required.
func (ar *AuthenticationRequest) ApplyRequestObject(roc *RequestObjectClaims, method jwt.SigningMethod) error {
// Basic consistency validation following spec at
// https://openid.net/specs/openid-connect-core-1_0.html#SignedRequestObject
if ok := ar.Scopes[oidc.ScopeOpenID]; !ok {
return ar.NewBadRequest(oidc.ErrorCodeOIDCInvalidRequestObject, "openid scope required when using the request parameter")
}
if roc.RawScope != "" {
ar.Scopes = make(map[string]bool)
// Parse scope directly, since the accociated authentication request
// has already parsed it when this is called.
for _, scope := range strings.Split(roc.RawScope, " ") {
ar.Scopes[scope] = true
}
}
if roc.RawResponseType != "" {
if roc.RawResponseType != ar.RawResponseType {
return ar.NewBadRequest(oidc.ErrorCodeOIDCInvalidRequestObject, "request object response_type mismatch")
}
}
if roc.ClientID != "" {
if roc.ClientID != ar.ClientID {
return ar.NewBadRequest(oidc.ErrorCodeOIDCInvalidRequestObject, "request object client_id mismatch")
}
}
if method != jwt.SigningMethodNone {
// Additional claim validation when signed. The spec says that iss and
// aud SHOULD have defined values. So for now we do not enforce here.
}
// Apply rest of the provided request object values to the accociated
// authentication request.
if roc.Claims != nil {
// NOTE(longsleep): Overwrite request claims with the signed claims
// from the request object. This ensures that only signed claims are
// processed if any have been given. If no signed claims have been
// given, the unsigned claims are kept, leaving it to further checks
// to ensure that only signed claims are used by checking that the
// roc object has claims.
ar.Claims = roc.Claims
}
if roc.RawRedirectURI != "" {
ar.RawRedirectURI = roc.RawRedirectURI
}
if roc.State != "" {
ar.State = roc.State
}
if roc.Nonce != "" {
ar.Nonce = roc.Nonce
}
if roc.RawPrompt != "" {
ar.RawPrompt = roc.RawPrompt
}
if roc.RawIDTokenHint != "" {
ar.RawIDTokenHint = roc.RawIDTokenHint
}
if roc.RawMaxAge != "" {
ar.RawMaxAge = roc.RawMaxAge
}
if roc.RawRegistration != "" {
ar.RawRegistration = roc.RawRegistration
}
if roc.CodeChallengeMethod != "" {
ar.CodeChallengeMethod = roc.CodeChallengeMethod
}
if roc.CodeChallenge != "" {
ar.CodeChallenge = roc.CodeChallenge
}
return nil
}
// Validate validates the request data of the accociated authentication request.
func (ar *AuthenticationRequest) Validate(keyFunc jwt.Keyfunc) error {
switch ar.RawResponseType {
case oidc.ResponseTypeCode:
// Code flow.
// breaks
case oidc.ResponseTypeCodeIDToken:
// Hybgrid flow.
if _, ok := ar.Scopes[oidc.ScopeOpenID]; !ok {
return ar.NewBadRequest(oidc.ErrorCodeOAuth2InvalidRequest, "missing openid scope in request")
}
// breaks
case oidc.ResponseTypeCodeToken:
// Hybgrid flow.
// breaks
case oidc.ResponseTypeCodeIDTokenToken:
// Hybgrid flow.
if _, ok := ar.Scopes[oidc.ScopeOpenID]; !ok {
return ar.NewBadRequest(oidc.ErrorCodeOAuth2InvalidRequest, "missing openid scope in request")
}
// breaks
case oidc.ResponseTypeIDToken:
// Implicit flow.
if _, ok := ar.Scopes[oidc.ScopeOpenID]; !ok {
return ar.NewBadRequest(oidc.ErrorCodeOAuth2InvalidRequest, "missing openid scope in request")
}
fallthrough
case oidc.ResponseTypeIDTokenToken:
// Implicit flow with access token.
if _, ok := ar.Scopes[oidc.ScopeOpenID]; !ok {
return ar.NewBadRequest(oidc.ErrorCodeOAuth2InvalidRequest, "missing openid scope in request")
}
if ar.Nonce == "" {
return ar.NewError(oidc.ErrorCodeOAuth2InvalidRequest, "nonce is required for implicit flow")
}
case oidc.ResponseTypeToken:
// OAuth2 flow implicit grant.
// breaks
default:
return ar.NewError(oidc.ErrorCodeOAuth2UnsupportedResponseType, "")
}
// Additional checks for flows with code.
if ar.Flow == oidc.FlowCode || ar.Flow == oidc.FlowHybrid {
switch ar.CodeChallengeMethod {
case "":
// breaks
case oidc.S256CodeChallengeMethod:
// breaks
case oidc.PlainCodeChallengeMethod:
// Plain is discouraged, and thus not supported.
fallthrough
default:
return ar.NewBadRequest(oidc.ErrorCodeOAuth2InvalidRequest, "transform algorithm not supported")
}
}
if _, hasNonePrompt := ar.Prompts[oidc.PromptNone]; hasNonePrompt {
if len(ar.Prompts) > 1 {
// Cannot have other prompts if none is requested.
return ar.NewError(oidc.ErrorCodeOAuth2InvalidRequest, "cannot request other prompts together with none")
}
}
if ar.ClientID == "" {
return ar.NewBadRequest(oidc.ErrorCodeOAuth2InvalidRequest, "missing client_id")
}
// TODO(longsleep): implement client_id white list.
if ar.RedirectURI == nil || !ar.RedirectURI.IsAbs() {
return ar.NewBadRequest(oidc.ErrorCodeOAuth2InvalidRequest, "invalid or missing redirect_uri")
}
if ar.RawIDTokenHint != "" {
parser := jwt.NewParser(jwt.WithoutClaimsValidation())
idTokenHint, err := parser.ParseWithClaims(ar.RawIDTokenHint, &konnectoidc.IDTokenClaims{}, func(token *jwt.Token) (interface{}, error) {
if keyFunc != nil {
return keyFunc(token)
}
return nil, fmt.Errorf("Not validated")
})
if err != nil {
return ar.NewBadRequest(oidc.ErrorCodeOAuth2InvalidRequest, err.Error())
}
ar.IDTokenHint = idTokenHint
}
// Offline access validation.
// http://openid.net/specs/openid-connect-core-1_0.html#OfflineAccess
if ok, _ := ar.Scopes[oidc.ScopeOfflineAccess]; ok {
if _, withCodeResponseType := ar.ResponseTypes[oidc.ResponseTypeCode]; !withCodeResponseType {
// Ignore the offline_access request unless the Client is using a
// response_type value that would result in an Authorization Code
// being returned.
delete(ar.Scopes, oidc.ScopeOfflineAccess)
}
}
if ar.RawRequestURI != "" {
return ar.NewError(oidc.ErrorCodeOIDCRequestURINotSupported, "")
}
if ar.RawRegistration != "" {
return ar.NewError(oidc.ErrorCodeOIDCRegistrationNotSupported, "")
}
return nil
}
// Verify checks that the passed parameters match the accociated requirements.
func (ar *AuthenticationRequest) Verify(userID string) error {
if ar.IDTokenHint != nil {
// Compare userID with IDTokenHint.
if userID != ar.IDTokenHint.Claims.(*konnectoidc.IDTokenClaims).Subject {
return ar.NewError(oidc.ErrorCodeOIDCLoginRequired, "userid mismatch")
}
}
return nil
}
// NewError creates a new error with id and string and the associated request's
// state.
func (ar *AuthenticationRequest) NewError(id string, description string) *AuthenticationError {
return &AuthenticationError{
ErrorID: id,
ErrorDescription: description,
State: ar.State,
}
}
// NewBadRequest creates a new error with id and string and the associated
// request's state.
func (ar *AuthenticationRequest) NewBadRequest(id string, description string) *AuthenticationBadRequest {
return &AuthenticationBadRequest{
ErrorID: id,
ErrorDescription: description,
State: ar.State,
}
}
// AuthenticationSuccess holds the outgoind data for a successful OpenID
// Connect 1.0 authorize request as specified at
// http://openid.net/specs/openid-connect-core-1_0.html#AuthResponse and
// http://openid.net/specs/openid-connect-core-1_0.html#ImplicitAuthResponse.
// https://openid.net/specs/openid-connect-session-1_0.html#CreatingUpdatingSessions
type AuthenticationSuccess struct {
Code string `url:"code,omitempty"`
AccessToken string `url:"access_token,omitempty"`
TokenType string `url:"token_type,omitempty"`
IDToken string `url:"id_token,omitempty"`
State string `url:"state"`
ExpiresIn int64 `url:"expires_in,omitempty"`
Scope string `url:"scope,omitempty"`
SessionState string `url:"session_state,omitempty"`
}
// AuthenticationError holds the outgoind data for a failed OpenID
// Connect 1.0 authorize request as specified at
// http://openid.net/specs/openid-connect-core-1_0.html#AuthError and
// http://openid.net/specs/openid-connect-core-1_0.html#ImplicitAuthError.
type AuthenticationError struct {
ErrorID string `url:"error" json:"error"`
ErrorDescription string `url:"error_description,omitempty" json:"error_description,omitempty"`
State string `url:"state,omitempty" json:"state,omitempty"`
}
// Error interface implementation.
func (ae *AuthenticationError) Error() string {
return ae.ErrorID
}
// Description implements ErrorWithDescription interface.
func (ae *AuthenticationError) Description() string {
return ae.ErrorDescription
}
// AuthenticationBadRequest holds the outgoing data for a failed OpenID Connect
// 1.0 authorize request with bad request parameters which make it impossible to
// continue with normal auth.
type AuthenticationBadRequest struct {
ErrorID string `url:"error" json:"error"`
ErrorDescription string `url:"error_description,omitempty" json:"error_description,omitempty"`
State string `url:"state,omitempty" json:"state,omitempty"`
}
// Error interface implementation.
func (ae *AuthenticationBadRequest) Error() string {
return ae.ErrorID
}
// Description implements ErrorWithDescription interface.
func (ae *AuthenticationBadRequest) Description() string {
return ae.ErrorDescription
}
+193
View File
@@ -0,0 +1,193 @@
/*
* Copyright 2017-2019 Kopano and its licensors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package payload
import (
"encoding/json"
"strings"
"github.com/libregraph/oidc-go"
)
var scopedClaims = map[string]string{
oidc.NameClaim: oidc.ScopeProfile,
oidc.FamilyNameClaim: oidc.ScopeProfile,
oidc.GivenNameClaim: oidc.ScopeProfile,
oidc.MiddleNameClaim: oidc.ScopeProfile,
oidc.PreferredUsernameClaim: oidc.ScopeProfile,
oidc.ProfileClaim: oidc.ScopeProfile,
oidc.PictureClaim: oidc.ScopeProfile,
oidc.WebsiteClaim: oidc.ScopeProfile,
oidc.GenderClaim: oidc.ScopeProfile,
oidc.BirthdateClaim: oidc.ScopeProfile,
oidc.ZoneinfoClaim: oidc.ScopeProfile,
oidc.UpdatedAtClaim: oidc.ScopeProfile,
oidc.EmailClaim: oidc.ScopeEmail,
oidc.EmailVerifiedClaim: oidc.ScopeEmail,
}
// GetScopeForClaim returns the known scope if any for the provided claim name.
func GetScopeForClaim(claim string) (string, bool) {
scope, ok := scopedClaims[claim]
return scope, ok
}
// ClaimsRequest define the base claims structure for OpenID Connect claims
// request parameter value as specified at
// https://openid.net/specs/openid-connect-core-1_0.html#ClaimsParameter - in
// addition a Konnect specific pass thru value can be used to pass through any
// application specific values to access and reqfresh tokens.
type ClaimsRequest struct {
UserInfo *ClaimsRequestMap `json:"userinfo,omitempty"`
IDToken *ClaimsRequestMap `json:"id_token,omitempty"`
Passthru json.RawMessage `json:"passthru,omitempty"`
}
// ApplyScopes removes all claims requests from the accociated claims request
// which are not mapped to one of the provided approved scopes.
func (cr *ClaimsRequest) ApplyScopes(approvedScopes map[string]bool) error {
if cr.UserInfo != nil {
for claim := range *cr.UserInfo {
if approved := approvedScopes[scopedClaims[claim]]; !approved {
delete(*cr.UserInfo, claim)
}
}
}
if cr.IDToken != nil {
for claim := range *cr.IDToken {
if approved := approvedScopes[scopedClaims[claim]]; !approved {
delete(*cr.IDToken, claim)
}
}
}
return nil
}
// Scopes adds all scopes of the accociated claims requests claims to
// the provied scopes mapping safe the scopes already defined in the provided
// excluded scopes mapping.
func (cr *ClaimsRequest) Scopes(excludedScopes map[string]bool) []string {
scopesMap := make(map[string]bool)
if cr.UserInfo != nil {
for claim := range *cr.UserInfo {
scope := scopedClaims[claim]
if _, excluded := excludedScopes[scope]; !excluded {
scopesMap[scope] = true
}
}
}
if cr.IDToken != nil {
for claim := range *cr.IDToken {
scope := scopedClaims[claim]
if _, excluded := excludedScopes[scope]; !excluded {
scopesMap[scope] = true
}
}
}
scopes := make([]string, 0)
for scope := range scopesMap {
scopes = append(scopes, scope)
}
return scopes
}
// ClaimsRequestMap defines a mapping of claims request values used with
// OpenID Connect claims request parameter values.
type ClaimsRequestMap map[string]*ClaimsRequestValue
// ScopesMap returns a map of scopes defined by the claims in tha associated map.
func (crm *ClaimsRequestMap) ScopesMap(excludedScopes map[string]bool) map[string]bool {
scopesMap := make(map[string]bool)
for claim := range *crm {
scope := scopedClaims[claim]
if _, excluded := excludedScopes[scope]; !excluded {
scopesMap[scope] = true
}
}
return scopesMap
}
// Get returns the accociated maps claim value identified by the provided name.
func (crm ClaimsRequestMap) Get(claim string) (*ClaimsRequestValue, bool) {
value, ok := crm[claim]
return value, ok
}
// GetStringValue returns the accociated maps claim value identified by the
// provided name as string value.
func (crm ClaimsRequestMap) GetStringValue(claim string) (string, bool) {
value, ok := crm.Get(claim)
if !ok {
return "", false
}
s, ok := value.Value.(string)
return s, ok
}
// ClaimsRequestValue is the claims request detail definition of an OpenID
// Connect claims request parameter value.
type ClaimsRequestValue struct {
Essential bool `json:"essential,omitempty"`
Value interface{} `json:"value,omitempty"`
Values []interface{} `json:"values,omitempty"`
}
// Match returns true of the provided value is contained inside the accociated
// request values values or value.
func (crv *ClaimsRequestValue) Match(value interface{}) bool {
if len(crv.Values) == 0 {
return value == crv.Value
}
for _, v := range crv.Values {
if v == value {
return true
}
}
return false
}
// ScopesValue is a string array with JSON marshal to/from a space separated
// single string value.
type ScopesValue []string
func (sv ScopesValue) MarshalJSON() ([]byte, error) {
result := strings.Join(sv, " ")
return json.Marshal(&result)
}
func (sv *ScopesValue) UnmarshalJSON(data []byte) error {
var parsed string
err := json.Unmarshal(data, &parsed)
if err != nil {
return err
}
result := ScopesValue(strings.Split(parsed, " "))
*sv = result
return nil
}
+132
View File
@@ -0,0 +1,132 @@
/*
* Copyright 2017-2019 Kopano and its licensors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package payload
import (
"fmt"
"net/http"
"net/url"
"github.com/golang-jwt/jwt/v5"
"github.com/libregraph/oidc-go"
konnectoidc "github.com/libregraph/lico/oidc"
)
// EndSessionRequest holds the incoming parameters and request data for OpenID
// Connect Session Management 1.0 RP initiaed logout requests as specified at
// https://openid.net/specs/openid-connect-session-1_0.html#RPLogout
type EndSessionRequest struct {
providerMetadata *oidc.WellKnown
RawIDTokenHint string `schema:"id_token_hint"`
RawPostLogoutRedirectURI string `schema:"post_logout_redirect_uri"`
State string `schema:"state"`
IDTokenHint *jwt.Token `schema:"-"`
PostLogoutRedirectURI *url.URL `schema:"-"`
}
// DecodeEndSessionRequest returns a EndSessionRequest holding the
// provided requests form data.
func DecodeEndSessionRequest(req *http.Request, providerMetadata *oidc.WellKnown) (*EndSessionRequest, error) {
return NewEndSessionRequest(req.Form, providerMetadata)
}
// NewEndSessionRequest returns a EndSessionRequest holding the
// provided url values.
func NewEndSessionRequest(values url.Values, providerMetadata *oidc.WellKnown) (*EndSessionRequest, error) {
esr := &EndSessionRequest{
providerMetadata: providerMetadata,
}
err := DecodeSchema(esr, values)
if err != nil {
return nil, err
}
esr.PostLogoutRedirectURI, _ = url.Parse(esr.RawPostLogoutRedirectURI)
return esr, nil
}
// Validate validates the request data of the accociated endSession request.
func (esr *EndSessionRequest) Validate(keyFunc jwt.Keyfunc) error {
if esr.RawIDTokenHint != "" {
parser := jwt.NewParser(jwt.WithoutClaimsValidation())
idTokenHint, err := parser.ParseWithClaims(esr.RawIDTokenHint, &konnectoidc.IDTokenClaims{}, func(token *jwt.Token) (interface{}, error) {
if keyFunc != nil {
return keyFunc(token)
}
return nil, fmt.Errorf("Not validated")
})
if err != nil {
return esr.NewBadRequest(oidc.ErrorCodeOAuth2InvalidRequest, err.Error())
}
esr.IDTokenHint = idTokenHint
}
return nil
}
// Verify checks that the passed parameters match the accociated requirements.
func (esr *EndSessionRequest) Verify(userID string) error {
if esr.IDTokenHint != nil {
// Compare userID with IDTokenHint.
if userID != esr.IDTokenHint.Claims.(*konnectoidc.IDTokenClaims).Subject {
return esr.NewBadRequest(oidc.ErrorCodeOAuth2InvalidRequest, "userid mismatch")
}
}
return nil
}
// NewError creates a new error with id and string and the associated request's
// state.
func (esr *EndSessionRequest) NewError(id string, description string) *AuthenticationError {
return &AuthenticationError{
ErrorID: id,
ErrorDescription: description,
State: esr.State,
}
}
// NewBadRequest creates a new error with id and string and the associated
// request's state.
func (esr *EndSessionRequest) NewBadRequest(id string, description string) *AuthenticationBadRequest {
return &AuthenticationBadRequest{
ErrorID: id,
ErrorDescription: description,
State: esr.State,
}
}
func (esr *EndSessionRequest) MakeRedirectEndSessionRequestURL() *url.URL {
if esr.PostLogoutRedirectURI == nil || esr.PostLogoutRedirectURI.String() == "" {
return nil
}
if esr.State == "" {
return esr.PostLogoutRedirectURI
}
uri, _ := url.Parse(esr.PostLogoutRedirectURI.String())
query := uri.Query()
query.Add("state", esr.State)
uri.RawQuery = query.Encode()
return uri
}
+315
View File
@@ -0,0 +1,315 @@
/*
* Copyright 2017-2019 Kopano and its licensors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package payload
import (
"encoding/json"
"fmt"
"net/http"
"net/url"
"strings"
"github.com/golang-jwt/jwt/v5"
"github.com/libregraph/oidc-go"
"github.com/mendsley/gojwk"
"github.com/libregraph/lico/identity/clients"
konnectoidc "github.com/libregraph/lico/oidc"
)
// ClientRegistrationRequest holds the incoming request data for the OpenID
// Connect Dynamic Client Registration 1.0 client registration endpoint as
// specified at https://openid.net/specs/openid-connect-registration-1_0.html#ClientRegistration and
// https://openid.net/specs/openid-connect-session-1_0.html#DynRegRegistrations
type ClientRegistrationRequest struct {
RedirectURIs []string `json:"redirect_uris"`
ResponseTypes []string `json:"response_types"`
GrantTypes []string `json:"grant_types"`
ApplicationType string `json:"application_type"`
Contacts []string `json:"contacts"`
ClientName string `json:"client_name"`
ClientURI string `json:"client_uri"`
RawJWKS json.RawMessage `json:"jwks"`
RawIDTokenSignedResponseAlg string `json:"id_token_signed_response_alg"`
RawUserInfoSignedResponseAlg string `json:"userinfo_signed_response_alg"`
RawRequestObjectSigningAlg string `json:"request_object_signing_alg"`
RawTokenEndpointAuthMethod string `json:"token_endpoint_auth_method"`
RawTokenEndpointAuthSigningAlg string `json:"token_endpoint_auth_signing_alg"`
PostLogoutRedirectURIs []string `json:"post_logout_redirect_uris"`
JWKS *gojwk.Key `json:"-"`
}
// DecodeClientRegistrationRequest returns a ClientRegistrationRequest holding
// the provided request's data.
func DecodeClientRegistrationRequest(req *http.Request) (*ClientRegistrationRequest, error) {
contentType := req.Header.Get("Content-Type")
if !strings.HasPrefix(contentType, "application/json") {
return nil, fmt.Errorf("invalid content-type")
}
decoder := json.NewDecoder(req.Body)
var crr ClientRegistrationRequest
err := decoder.Decode(&crr)
if err != nil {
return nil, fmt.Errorf("failed to decode client registration request: %v", err)
}
if crr.RawJWKS != nil {
jwks, err := gojwk.Unmarshal(crr.RawJWKS)
if err != nil {
return nil, fmt.Errorf("failed to decode client registration request jwks: %v", err)
}
// Only use keys.
crr.JWKS = &gojwk.Key{
Keys: jwks.Keys,
}
}
return &crr, err
}
// Validate validates the request data of the accociated client registration
// request and fills in default data where required.
func (crr *ClientRegistrationRequest) Validate() error {
if len(crr.RedirectURIs) == 0 {
return konnectoidc.NewOAuth2Error(oidc.ErrorCodeOIDCInvalidRedirectURI, "redirect_uris required")
}
// Validate and filter response_type.
if len(crr.ResponseTypes) == 0 {
crr.ResponseTypes = []string{oidc.ResponseTypeCode}
}
requiredGrantTypes := make(map[string]bool)
responseTypes := make([]string, 0)
for _, responseType := range crr.ResponseTypes {
switch responseType {
case oidc.ResponseTypeCode:
requiredGrantTypes[oidc.GrantTypeAuthorizationCode] = true
responseTypes = append(responseTypes, responseType)
// breaks
case oidc.ResponseTypeCodeIDToken:
fallthrough
case oidc.ResponseTypeCodeIDTokenToken:
fallthrough
case oidc.ResponseTypeCodeToken:
requiredGrantTypes[oidc.GrantTypeAuthorizationCode] = true
requiredGrantTypes[oidc.GrantTypeImplicit] = true
responseTypes = append(responseTypes, responseType)
// breaks
case oidc.ResponseTypeIDToken:
fallthrough
case oidc.ResponseTypeIDTokenToken:
requiredGrantTypes[oidc.GrantTypeAuthorizationCode] = true
requiredGrantTypes[oidc.GrantTypeImplicit] = true
responseTypes = append(responseTypes, responseType)
// breaks
case oidc.ResponseTypeToken:
responseTypes = append(responseTypes, responseType)
default:
}
}
crr.ResponseTypes = responseTypes
// Filter and validate grant_types.
if len(crr.GrantTypes) == 0 {
crr.GrantTypes = []string{oidc.GrantTypeAuthorizationCode}
}
grantTypes := make([]string, 0)
registeredGrantTypes := make(map[string]bool)
for _, grantType := range crr.GrantTypes {
switch grantType {
case oidc.GrantTypeAuthorizationCode:
fallthrough
case oidc.GrantTypeImplicit:
fallthrough
case oidc.GrantTypeRefreshToken:
registeredGrantTypes[grantType] = true
grantTypes = append(grantTypes, grantType)
default:
}
}
for grantType := range requiredGrantTypes {
if ok := registeredGrantTypes[grantType]; !ok {
return konnectoidc.NewOAuth2Error(oidc.ErrorCodeOIDCInvalidClientMetadata, "grant_types conflict with response_types")
}
}
if crr.ApplicationType == "" {
crr.ApplicationType = oidc.ApplicationTypeWeb
}
switch crr.ApplicationType {
case oidc.ApplicationTypeWeb:
// Web Clients using the OAuth Implicit Grant Type MUST only register
// URLs using the https scheme as redirect_uris; they MUST NOT use
// localhost as the hostname.
for _, uriString := range crr.RedirectURIs {
uri, err := url.Parse(uriString)
if err != nil {
return konnectoidc.NewOAuth2Error(oidc.ErrorCodeOIDCInvalidRedirectURI, "failed to parse redirect_uris")
}
if ok := registeredGrantTypes[oidc.GrantTypeImplicit]; ok {
if uri.Scheme != "https" {
return konnectoidc.NewOAuth2Error(oidc.ErrorCodeOIDCInvalidRedirectURI, "web clients must use https redirect_uris")
}
if clients.IsLocalNativeHostURI(uri) {
return konnectoidc.NewOAuth2Error(oidc.ErrorCodeOIDCInvalidRedirectURI, "web clients must not use localhost redirect_uris")
}
}
}
case oidc.ApplicationTypeNative:
// Native Clients MUST only register redirect_uris using custom URI
// schemes or URLs using the http: scheme with localhost as the hostname.
for _, uriString := range crr.RedirectURIs {
uri, err := url.Parse(uriString)
if err != nil {
return konnectoidc.NewOAuth2Error(oidc.ErrorCodeOIDCInvalidRedirectURI, "failed to parse redirect_uris")
}
if !clients.IsLocalNativeHTTPURI(uri) {
return konnectoidc.NewOAuth2Error(oidc.ErrorCodeOIDCInvalidRedirectURI, "native clients must only use localhost redirect_uris with http")
}
}
default:
return konnectoidc.NewOAuth2Error(oidc.ErrorCodeOIDCInvalidClientMetadata, "unknown application_type")
}
if crr.RawIDTokenSignedResponseAlg == "" {
crr.RawIDTokenSignedResponseAlg = jwt.SigningMethodRS256.Alg()
}
if crr.RawIDTokenSignedResponseAlg != "" {
alg := jwt.GetSigningMethod(crr.RawIDTokenSignedResponseAlg)
if alg == nil {
return konnectoidc.NewOAuth2Error(oidc.ErrorCodeOIDCInvalidClientMetadata, "unknown id_token_signed_response_alg")
}
}
if crr.RawUserInfoSignedResponseAlg != "" {
alg := jwt.GetSigningMethod(crr.RawUserInfoSignedResponseAlg)
if alg == nil {
return konnectoidc.NewOAuth2Error(oidc.ErrorCodeOIDCInvalidClientMetadata, "unknown userinfo_signed_response_alg")
}
}
if crr.RawRequestObjectSigningAlg != "" {
alg := jwt.GetSigningMethod(crr.RawRequestObjectSigningAlg)
if alg == nil {
return konnectoidc.NewOAuth2Error(oidc.ErrorCodeOIDCInvalidClientMetadata, "unknown request_object_signing_alg")
}
}
if crr.RawTokenEndpointAuthMethod == "" {
crr.RawTokenEndpointAuthMethod = oidc.AuthMethodClientSecretBasic
}
if crr.RawTokenEndpointAuthMethod != "" {
switch crr.RawTokenEndpointAuthMethod {
case oidc.AuthMethodClientSecretBasic:
// breaks
case oidc.AuthMethodNone:
// breaks
default:
return konnectoidc.NewOAuth2Error(oidc.ErrorCodeOIDCInvalidClientMetadata, "unsupported token_endpoint_auth_method")
}
}
if crr.RawTokenEndpointAuthSigningAlg != "" {
alg := jwt.GetSigningMethod(crr.RawTokenEndpointAuthSigningAlg)
if alg == nil {
return konnectoidc.NewOAuth2Error(oidc.ErrorCodeOIDCInvalidClientMetadata, "unknown token_endpoint_auth_signing_alg")
}
}
for _, uriString := range crr.PostLogoutRedirectURIs {
_, err := url.Parse(uriString)
if err != nil {
return konnectoidc.NewOAuth2Error(oidc.ErrorCodeOIDCInvalidClientMetadata, "failed to parse post_logout_redirect_uris")
}
}
if crr.JWKS != nil {
if len(crr.JWKS.Keys) == 0 {
crr.JWKS = nil
} else {
enc := false
empty := true
for _, key := range crr.JWKS.Keys {
switch key.Use {
case "":
if enc {
return konnectoidc.NewOAuth2Error(oidc.ErrorCodeOIDCInvalidClientMetadata, "jwks includes enc key and unset use key")
}
empty = true
key.Use = "sig"
case "enc":
enc = true
if empty {
return konnectoidc.NewOAuth2Error(oidc.ErrorCodeOIDCInvalidClientMetadata, "jwks includes enc key and unset use key")
}
}
}
}
}
return nil
}
// ClientRegistration returns new dynamic client registration data for the
// accociated client registration request.
func (crr *ClientRegistrationRequest) ClientRegistration() (*clients.ClientRegistration, error) {
cr := &clients.ClientRegistration{
Contacts: crr.Contacts,
Name: crr.ClientName,
URI: crr.ClientURI,
GrantTypes: crr.GrantTypes,
ApplicationType: crr.ApplicationType,
RedirectURIs: crr.RedirectURIs,
JWKS: crr.JWKS,
RawIDTokenSignedResponseAlg: crr.RawIDTokenSignedResponseAlg,
RawUserInfoSignedResponseAlg: crr.RawUserInfoSignedResponseAlg,
RawRequestObjectSigningAlg: crr.RawRequestObjectSigningAlg,
RawTokenEndpointAuthMethod: crr.RawTokenEndpointAuthMethod,
RawTokenEndpointAuthSigningAlg: crr.RawTokenEndpointAuthSigningAlg,
PostLogoutRedirectURIs: crr.PostLogoutRedirectURIs,
}
return cr, nil
}
// ClientRegistrationResponse holds the outgoing data for a successful OpenID
// Connect Dynamic Client Registration 1.0 clientregistration request as
// specified at https://openid.net/specs/openid-connect-registration-1_0.html#RegistrationResponse
type ClientRegistrationResponse struct {
ClientID string `json:"client_id"`
ClientSecret string `json:"client_secret,omitempty"`
ClientIDIssuedAt int64 `json:"client_id_issued_at,omitempty"`
ClientSecretExpiresAt int64 `json:"client_secret_expires_at"`
// Include validated request data.
ClientRegistrationRequest
}
+69
View File
@@ -0,0 +1,69 @@
/*
* Copyright 2017-2019 Kopano and its licensors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package payload
import (
"errors"
"github.com/golang-jwt/jwt/v5"
"github.com/libregraph/lico/identity/clients"
)
// RequestObjectClaims holds the incoming request object claims provided as
// JWT via request parameter to OpenID Connect 1.0 authorization endpoint
// requests specified at
// https://openid.net/specs/openid-connect-core-1_0.html#JWTRequests
type RequestObjectClaims struct {
jwt.RegisteredClaims
RawScope string `json:"scope"`
Claims *ClaimsRequest `json:"claims"`
RawResponseType string `json:"response_type"`
ResponseMode string `json:"response_mode"`
ClientID string `json:"client_id"`
RawRedirectURI string `json:"redirect_uri"`
State string `json:"state"`
Nonce string `json:"nonce"`
RawPrompt string `json:"prompt"`
RawIDTokenHint string `json:"id_token_hint"`
RawMaxAge string `json:"max_age"`
RawRegistration string `json:"registration"`
CodeChallenge string `json:"code_challenge"`
CodeChallengeMethod string `json:"code_challenge_method"`
client *clients.Secured
}
// SetSecure sets the provided client as owner of the accociated claims.
func (roc *RequestObjectClaims) SetSecure(client *clients.Secured) error {
if roc.ClientID != client.ID {
return errors.New("client ID mismatch")
}
roc.client = client
return nil
}
// Secure returns the accociated secure client or nil if not secure.
func (roc *RequestObjectClaims) Secure() *clients.Secured {
return roc.client
}
+55
View File
@@ -0,0 +1,55 @@
/*
* Copyright 2017-2019 Kopano and its licensors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package payload
import (
"encoding/json"
"reflect"
"github.com/gorilla/schema"
)
var decoder = schema.NewDecoder()
var encoder = schema.NewEncoder()
// DecodeSchema decodes request form data into the provided dst schema struct.
func DecodeSchema(dst interface{}, src map[string][]string) error {
return decoder.Decode(dst, src)
}
// EncodeSchema encodes the provided src schema to the provided map.
func EncodeSchema(src interface{}, dst map[string][]string) error {
return encoder.Encode(src, dst)
}
// ConvertOIDCClaimsRequest is a converter function for oidc.ClaimsRequest data
// provided in URL schema.
func ConvertOIDCClaimsRequest(value string) reflect.Value {
v := ClaimsRequest{}
if err := json.Unmarshal([]byte(value), &v); err != nil {
return reflect.Value{}
}
return reflect.ValueOf(v)
}
func init() {
decoder.IgnoreUnknownKeys(true)
decoder.RegisterConverter(ClaimsRequest{}, ConvertOIDCClaimsRequest)
}
+30
View File
@@ -0,0 +1,30 @@
/*
* Copyright 2017-2019 Kopano and its licensors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package payload
// Session defines a Provider's session with a String identifier for a Session.
// This represents a Session of a User Agent or device for a logged-in End-User
// at an RP. Different ID values are used to identify distinct sessions. This
// is implemented as defined in the OIDC Front Channel logout extension
// https://openid.net/specs/openid-connect-frontchannel-1_0.html#OPLogout
type Session struct {
Version int
ID string
Sub string
Provider string
}
+168
View File
@@ -0,0 +1,168 @@
/*
* Copyright 2017-2019 Kopano and its licensors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package payload
import (
"encoding/base64"
"fmt"
"net/http"
"net/url"
"strings"
"github.com/golang-jwt/jwt/v5"
"github.com/libregraph/oidc-go"
konnectoidc "github.com/libregraph/lico/oidc"
)
// TokenRequest holds the incoming parameters and request data for
// the OpenID Connect 1.0 token endpoint as specified at
// http://openid.net/specs/openid-connect-core-1_0.html#TokenRequest
type TokenRequest struct {
providerMetadata *oidc.WellKnown
GrantType string `schema:"grant_type"`
Code string `schema:"code"`
RawRedirectURI string `schema:"redirect_uri"`
RawRefreshToken string `schema:"refresh_token"`
RawScope string `schema:"scope"`
ClientID string `schema:"client_id"`
ClientSecret string `schema:"client_secret"`
CodeVerifier string `schema:"code_verifier"`
RedirectURI *url.URL `schema:"-"`
RefreshToken *jwt.Token `schema:"-"`
Scopes map[string]bool `schema:"-"`
}
// DecodeTokenRequest return a TokenRequest holding the provided
// request's form data.
func DecodeTokenRequest(req *http.Request, providerMetadata *oidc.WellKnown) (*TokenRequest, error) {
tr, err := NewTokenRequest(req.PostForm, providerMetadata)
if err != nil {
return nil, err
}
var clientID string
var clientSecret string
auth := strings.SplitN(req.Header.Get("Authorization"), " ", 2)
switch auth[0] {
case "Basic":
// Support client_secret_basic authentication method.
if len(auth) != 2 {
return nil, fmt.Errorf("invalid Basic authorization header format")
}
var basic []byte
if basic, err = base64.StdEncoding.DecodeString(auth[1]); err != nil {
return nil, fmt.Errorf("invalid Basic authorization value: %w", err)
}
// Decode username as client ID and password as client secret. See
// https://tools.ietf.org/html/rfc6749#section-2.3.1 for details.
check := strings.SplitN(string(basic), ":", 2)
if len(check) == 2 {
// Data is encoded application/x-www-form-urlencoded UTF-8. See
// https://tools.ietf.org/html/rfc6749#appendix-B for details.
if clientID, err = url.QueryUnescape(check[0]); err == nil {
clientSecret, _ = url.QueryUnescape(check[1])
}
}
}
if tr.ClientID == "" {
if clientID == "" {
return nil, fmt.Errorf("client_id is missing")
}
// Use client ID and secret if no client_id was passed to the request directly.
tr.ClientID = clientID
tr.ClientSecret = clientSecret
} else if clientID != "" {
if tr.ClientID == clientID {
// Update the client secret, if the ID is a match. This replaces
// a directly given secret.
tr.ClientSecret = clientSecret
}
}
return tr, err
}
// NewTokenRequest returns a TokenRequest holding the provided url values.
func NewTokenRequest(values url.Values, providerMetadata *oidc.WellKnown) (*TokenRequest, error) {
tr := &TokenRequest{
providerMetadata: providerMetadata,
Scopes: make(map[string]bool),
}
err := DecodeSchema(tr, values)
if err != nil {
return nil, err
}
tr.RedirectURI, _ = url.Parse(tr.RawRedirectURI)
if tr.RawScope != "" {
for _, scope := range strings.Split(tr.RawScope, " ") {
tr.Scopes[scope] = true
}
}
return tr, nil
}
// Validate validates the request data of the accociated token request.
func (tr *TokenRequest) Validate(keyFunc jwt.Keyfunc, claims jwt.Claims) error {
switch tr.GrantType {
case oidc.GrantTypeAuthorizationCode:
// breaks
case oidc.GrantTypeRefreshToken:
if tr.RawRefreshToken != "" {
refreshToken, err := jwt.ParseWithClaims(tr.RawRefreshToken, claims, func(token *jwt.Token) (interface{}, error) {
if keyFunc != nil {
return keyFunc(token)
}
return nil, fmt.Errorf("Not validated")
})
if err != nil {
return konnectoidc.NewOAuth2Error(oidc.ErrorCodeOAuth2InvalidRequest, err.Error())
}
tr.RefreshToken = refreshToken
}
// breaks
default:
return konnectoidc.NewOAuth2Error(oidc.ErrorCodeOAuth2UnsupportedGrantType, "unsupported grant_type value")
}
return nil
}
// TokenSuccess holds the outgoing data for a successful OpenID
// Connect 1.0 token request as specified at
// http://openid.net/specs/openid-connect-core-1_0.html#TokenResponse.
type TokenSuccess struct {
AccessToken string `json:"access_token,omitempty"`
TokenType string `json:"token_type,omitempty"`
IDToken string `json:"id_token,omitempty"`
RefreshToken string `json:"refresh_token,omitempty"`
ExpiresIn int64 `json:"expires_in,omitempty"`
}
+30
View File
@@ -0,0 +1,30 @@
/*
* Copyright 2017-2019 Kopano and its licensors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package payload
import (
"github.com/libregraph/lico/oidc"
)
// UserInfoResponse defines the data returned from the OIDC UserInfo
// endpoint.
type UserInfoResponse struct {
oidc.UserInfoClaims
*oidc.ProfileClaims
*oidc.EmailClaims
}
+40
View File
@@ -0,0 +1,40 @@
/*
* Copyright 2017-2019 Kopano and its licensors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package payload
import (
"encoding/json"
)
// ToMap is a helper function to convert the provided payload struct to
// a map type which can be used to extend the payload data with additional fields.
func ToMap(payload interface{}) (map[string]interface{}, error) {
// NOTE(longsleep): This implementation sucks, marshal to JSON and unmarshal
// again - rly?
intermediate, err := json.Marshal(payload)
if err != nil {
return nil, err
}
claims := make(map[string]interface{})
err = json.Unmarshal(intermediate, &claims)
if err != nil {
return nil, err
}
return claims, nil
}