introspect

This commit is contained in:
Livio Amstutz 2021-02-10 16:42:01 +01:00
parent 134999bc33
commit 138da8a208
13 changed files with 305 additions and 98 deletions

View file

@ -27,6 +27,7 @@ var (
func main() { func main() {
clientID := os.Getenv("CLIENT_ID") clientID := os.Getenv("CLIENT_ID")
clientSecret := os.Getenv("CLIENT_SECRET") clientSecret := os.Getenv("CLIENT_SECRET")
keyPath := os.Getenv("KEY_PATH")
issuer := os.Getenv("ISSUER") issuer := os.Getenv("ISSUER")
port := os.Getenv("PORT") port := os.Getenv("PORT")
scopes := strings.Split(os.Getenv("SCOPES"), " ") scopes := strings.Split(os.Getenv("SCOPES"), " ")
@ -35,10 +36,19 @@ func main() {
redirectURI := fmt.Sprintf("http://localhost:%v%v", port, callbackPath) redirectURI := fmt.Sprintf("http://localhost:%v%v", port, callbackPath)
cookieHandler := utils.NewCookieHandler(key, key, utils.WithUnsecure()) cookieHandler := utils.NewCookieHandler(key, key, utils.WithUnsecure())
provider, err := rp.NewRelayingPartyOIDC(issuer, clientID, clientSecret, redirectURI, scopes,
rp.WithPKCE(cookieHandler), options := []rp.Option{
rp.WithVerifierOpts(rp.WithIssuedAtOffset(5*time.Second)), rp.WithCookieHandler(cookieHandler),
) rp.WithVerifierOpts(rp.WithIssuedAtOffset(5 * time.Second)),
}
if clientSecret == "" {
options = append(options, rp.WithPKCE(cookieHandler))
}
if keyPath != "" {
options = append(options, rp.WithClientKey(keyPath))
}
provider, err := rp.NewRelayingPartyOIDC(issuer, clientID, clientSecret, redirectURI, scopes, options...)
if err != nil { if err != nil {
logrus.Fatalf("error creating provider %s", err.Error()) logrus.Fatalf("error creating provider %s", err.Error())
} }

View file

@ -1,27 +1,56 @@
package oidc package oidc
import (
"golang.org/x/text/language"
)
const ( const (
DiscoveryEndpoint = "/.well-known/openid-configuration" DiscoveryEndpoint = "/.well-known/openid-configuration"
) )
type DiscoveryConfiguration struct { type DiscoveryConfiguration struct {
Issuer string `json:"issuer,omitempty"` Issuer string `json:"issuer,omitempty"`
AuthorizationEndpoint string `json:"authorization_endpoint,omitempty"` AuthorizationEndpoint string `json:"authorization_endpoint,omitempty"`
TokenEndpoint string `json:"token_endpoint,omitempty"` TokenEndpoint string `json:"token_endpoint,omitempty"`
IntrospectionEndpoint string `json:"introspection_endpoint,omitempty"` IntrospectionEndpoint string `json:"introspection_endpoint,omitempty"`
UserinfoEndpoint string `json:"userinfo_endpoint,omitempty"` UserinfoEndpoint string `json:"userinfo_endpoint,omitempty"`
EndSessionEndpoint string `json:"end_session_endpoint,omitempty"` RevocationEndpoint string `json:"revocation_endpoint,omitempty"`
CheckSessionIframe string `json:"check_session_iframe,omitempty"` EndSessionEndpoint string `json:"end_session_endpoint,omitempty"`
JwksURI string `json:"jwks_uri,omitempty"` CheckSessionIframe string `json:"check_session_iframe,omitempty"`
ScopesSupported []string `json:"scopes_supported,omitempty"` JwksURI string `json:"jwks_uri,omitempty"`
ResponseTypesSupported []string `json:"response_types_supported,omitempty"` ScopesSupported []string `json:"scopes_supported,omitempty"`
ResponseModesSupported []string `json:"response_modes_supported,omitempty"` ResponseTypesSupported []string `json:"response_types_supported"`
GrantTypesSupported []string `json:"grant_types_supported,omitempty"` ResponseModesSupported []string `json:"response_modes_supported,omitempty"`
SubjectTypesSupported []string `json:"subject_types_supported,omitempty"` GrantTypesSupported []GrantType `json:"grant_types_supported,omitempty"`
IDTokenSigningAlgValuesSupported []string `json:"id_token_signing_alg_values_supported,omitempty"` ACRValuesSupported []string `json:"acr_values_supported,omitempty"`
TokenEndpointAuthMethodsSupported []AuthMethod `json:"token_endpoint_auth_methods_supported,omitempty"` SubjectTypesSupported []string `json:"subject_types_supported,omitempty"`
CodeChallengeMethodsSupported []CodeChallengeMethod `json:"code_challenge_methods_supported,omitempty"` IDTokenSigningAlgValuesSupported []string `json:"id_token_signing_alg_values_supported,omitempty"`
ClaimsSupported []string `json:"claims_supported,omitempty"` IDTokenEncryptionAlgValuesSupported []string `json:"id_token_encryption_alg_values_supported,omitempty"`
IDTokenEncryptionEncValuesSupported []string `json:"id_token_encryption_enc_values_supported,omitempty"`
UserinfoSigningAlgValuesSupported []string `json:"userinfo_signing_alg_values_supported,omitempty"`
UserinfoEncryptionAlgValuesSupported []string `json:"userinfo_encryption_alg_values_supported,omitempty"`
UserinfoEncryptionEncValuesSupported []string `json:"userinfo_encryption_enc_values_supported,omitempty"`
RequestObjectSigningAlgValuesSupported []string `json:"request_object_signing_alg_values_supported,omitempty"`
RequestObjectEncryptionAlgValuesSupported []string `json:"request_object_encryption_alg_values_supported,omitempty"`
RequestObjectEncryptionEncValuesSupported []string `json:"request_object_encryption_enc_values_supported,omitempty"`
TokenEndpointAuthMethodsSupported []AuthMethod `json:"token_endpoint_auth_methods_supported,omitempty"`
TokenEndpointAuthSigningAlgValuesSupported []string `json:"token_endpoint_auth_signing_alg_values_supported,omitempty"`
RevocationEndpointAuthMethodsSupported []AuthMethod `json:"revocation_endpoint_auth_methods_supported,omitempty"`
RevocationEndpointAuthSigningAlgValuesSupported []string `json:"revocation_endpoint_auth_signing_alg_values_supported,omitempty"`
IntrospectionEndpointAuthMethodsSupported []AuthMethod `json:"introspection_endpoint_auth_methods_supported,omitempty"`
IntrospectionEndpointAuthSigningAlgValuesSupported []string `json:"introspection_endpoint_auth_signing_alg_values_supported,omitempty"`
DisplayValuesSupported []Display `json:"display_values_supported,omitempty"`
ClaimTypesSupported []string `json:"claim_types_supported,omitempty"`
ClaimsSupported []string `json:"claims_supported,omitempty"`
CodeChallengeMethodsSupported []CodeChallengeMethod `json:"code_challenge_methods_supported,omitempty"`
ServiceDocumentation string `json:"service_documentation,omitempty"`
ClaimsLocalesSupported []language.Tag `json:"claims_locales_supported,omitempty"`
UILocalesSupported []language.Tag `json:"ui_locales_supported,omitempty"`
RequestParameterSupported bool `json:"request_parameter_supported,omitempty"`
RequestURIParameterSupported bool `json:"request_uri_parameter_supported"` //no omitempty because: If omitted, the default value is true
RequireRequestURIRegistration bool `json:"require_request_uri_registration,omitempty"`
OPPolicyURI string `json:"op_policy_uri,omitempty"`
OPTermsOfServiceURI string `json:"op_tos_uri,omitempty"`
} }
type AuthMethod string type AuthMethod string
@ -32,3 +61,7 @@ const (
AuthMethodNone AuthMethod = "none" AuthMethodNone AuthMethod = "none"
AuthMethodPrivateKeyJWT AuthMethod = "private_key_jwt" AuthMethodPrivateKeyJWT AuthMethod = "private_key_jwt"
) )
const (
GrantTypeImplicit GrantType = "implicit"
)

View file

@ -12,10 +12,17 @@ type IntrospectionRequest struct {
Token string `schema:"token"` Token string `schema:"token"`
} }
type ClientAssertionParams struct {
ClientAssertion string `schema:"client_assertion"`
ClientAssertionType string `schema:"client_assertion_type"`
}
type IntrospectionResponse interface { type IntrospectionResponse interface {
UserInfoSetter UserInfoSetter
SetActive(bool) SetActive(bool)
IsActive() bool IsActive() bool
SetScopes(scopes Scope)
SetClientID(id string)
} }
func NewIntrospectionResponse() IntrospectionResponse { func NewIntrospectionResponse() IntrospectionResponse {
@ -23,19 +30,30 @@ func NewIntrospectionResponse() IntrospectionResponse {
} }
type introspectionResponse struct { type introspectionResponse struct {
Active bool `json:"active"` Active bool `json:"active"`
Subject string `json:"sub,omitempty"` Scope Scope `json:"scope,omitempty"`
ClientID string `json:"client_id,omitempty"`
Subject string `json:"sub,omitempty"`
userInfoProfile userInfoProfile
userInfoEmail userInfoEmail
userInfoPhone userInfoPhone
Address UserInfoAddress `json:"address,omitempty"`
claims map[string]interface{} Address UserInfoAddress `json:"address,omitempty"`
claims map[string]interface{}
} }
func (u *introspectionResponse) IsActive() bool { func (u *introspectionResponse) IsActive() bool {
return u.Active return u.Active
} }
func (u *introspectionResponse) SetScopes(scope Scope) {
u.Scope = scope
}
func (u *introspectionResponse) SetClientID(id string) {
u.ClientID = id
}
func (u *introspectionResponse) GetSubject() string { func (u *introspectionResponse) GetSubject() string {
return u.Subject return u.Subject
} }

View file

@ -427,7 +427,7 @@ func NewJWTProfileAssertionStringFromFileData(data []byte, audience []string) (s
if err != nil { if err != nil {
return "", err return "", err
} }
return generateJWTProfileToken(NewJWTProfileAssertion(keyData.UserID, keyData.KeyID, audience, []byte(keyData.Key))) return GenerateJWTProfileToken(NewJWTProfileAssertion(keyData.UserID, keyData.KeyID, audience, []byte(keyData.Key)))
} }
func NewJWTProfileAssertionFromFileData(data []byte, audience []string) (*JWTProfileAssertion, error) { func NewJWTProfileAssertionFromFileData(data []byte, audience []string) (*JWTProfileAssertion, error) {
@ -473,7 +473,7 @@ func AppendClientIDToAudience(clientID string, audience []string) []string {
return append(audience, clientID) return append(audience, clientID)
} }
func generateJWTProfileToken(assertion *JWTProfileAssertion) (string, error) { func GenerateJWTProfileToken(assertion *JWTProfileAssertion) (string, error) {
privateKey, err := bytesToPrivateKey(assertion.PrivateKey) privateKey, err := bytesToPrivateKey(assertion.PrivateKey)
if err != nil { if err != nil {
return "", err return "", err

View file

@ -59,6 +59,7 @@ type Prompt string
type ResponseType string type ResponseType string
type Scopes []string type Scopes []string
type Scope []string //TODO: hurst?
func (s Scopes) Encode() string { func (s Scopes) Encode() string {
return strings.Join(s, " ") return strings.Join(s, " ")
@ -73,6 +74,19 @@ func (s *Scopes) MarshalText() ([]byte, error) {
return []byte(s.Encode()), nil return []byte(s.Encode()), nil
} }
func (s *Scope) MarshalJSON() ([]byte, error) {
return json.Marshal(Scopes(*s).Encode())
}
func (s *Scope) UnmarshalJSON(data []byte) error {
var str string
if err := json.Unmarshal(data, &str); err != nil {
return err
}
*s = Scope(strings.Split(str, " "))
return nil
}
type Time time.Time type Time time.Time
func (t *Time) UnmarshalJSON(data []byte) error { func (t *Time) UnmarshalJSON(data []byte) error {

View file

@ -6,8 +6,6 @@ import (
"time" "time"
"golang.org/x/text/language" "golang.org/x/text/language"
"github.com/caos/oidc/pkg/utils"
) )
type UserInfo interface { type UserInfo interface {
@ -351,11 +349,17 @@ func (i *userinfo) MarshalJSON() ([]byte, error) {
return b, nil return b, nil
} }
claims, err := json.Marshal(i.claims) err = json.Unmarshal(b, &i.claims)
if err != nil { if err != nil {
return nil, fmt.Errorf("jws: invalid map of custom claims %v", i.claims) return nil, fmt.Errorf("jws: invalid map of custom claims %v", i.claims)
} }
return utils.ConcatenateJSON(b, claims)
return json.Marshal(i.claims)
//claims, err := json.Marshal(i.claims)
//if err != nil {
// return nil, fmt.Errorf("jws: invalid map of custom claims %v", i.claims)
//}
//return utils.ConcatenateJSON(b, claims)
} }
func (i *userinfo) UnmarshalJSON(data []byte) error { func (i *userinfo) UnmarshalJSON(data []byte) error {
@ -372,6 +376,10 @@ func (i *userinfo) UnmarshalJSON(data []byte) error {
i.UpdatedAt = Time(time.Unix(a.UpdatedAt, 0).UTC()) i.UpdatedAt = Time(time.Unix(a.UpdatedAt, 0).UTC())
if err := json.Unmarshal(data, &i.claims); err != nil {
return err
}
return nil return nil
} }

View file

@ -3,6 +3,8 @@ package op
import ( import (
"net/http" "net/http"
"golang.org/x/text/language"
"github.com/caos/oidc/pkg/oidc" "github.com/caos/oidc/pkg/oidc"
"github.com/caos/oidc/pkg/utils" "github.com/caos/oidc/pkg/utils"
) )
@ -24,17 +26,22 @@ func CreateDiscoveryConfig(c Configuration, s Signer) *oidc.DiscoveryConfigurati
TokenEndpoint: c.TokenEndpoint().Absolute(c.Issuer()), TokenEndpoint: c.TokenEndpoint().Absolute(c.Issuer()),
IntrospectionEndpoint: c.IntrospectionEndpoint().Absolute(c.Issuer()), IntrospectionEndpoint: c.IntrospectionEndpoint().Absolute(c.Issuer()),
UserinfoEndpoint: c.UserinfoEndpoint().Absolute(c.Issuer()), UserinfoEndpoint: c.UserinfoEndpoint().Absolute(c.Issuer()),
EndSessionEndpoint: c.EndSessionEndpoint().Absolute(c.Issuer()), //RevocationEndpoint: c.RevocationEndpoint().Absolute(c.Issuer()),
EndSessionEndpoint: c.EndSessionEndpoint().Absolute(c.Issuer()),
// CheckSessionIframe: c.TokenEndpoint().Absolute(c.Issuer())(c.CheckSessionIframe), // CheckSessionIframe: c.TokenEndpoint().Absolute(c.Issuer())(c.CheckSessionIframe),
JwksURI: c.KeysEndpoint().Absolute(c.Issuer()), JwksURI: c.KeysEndpoint().Absolute(c.Issuer()),
ScopesSupported: Scopes(c), ScopesSupported: Scopes(c),
ResponseTypesSupported: ResponseTypes(c), ResponseTypesSupported: ResponseTypes(c),
GrantTypesSupported: GrantTypes(c), //ResponseModesSupported:
ClaimsSupported: SupportedClaims(c), GrantTypesSupported: GrantTypes(c),
IDTokenSigningAlgValuesSupported: SigAlgorithms(s), //ACRValuesSupported: ACRValues(c),
SubjectTypesSupported: SubjectTypes(c), SubjectTypesSupported: SubjectTypes(c),
TokenEndpointAuthMethodsSupported: AuthMethods(c), IDTokenSigningAlgValuesSupported: SigAlgorithms(s),
CodeChallengeMethodsSupported: CodeChallengeMethods(c), TokenEndpointAuthMethodsSupported: AuthMethodsTokenEndpoint(c),
IntrospectionEndpointAuthMethodsSupported: AuthMethodsIntrospectionEndpoint(c),
ClaimsSupported: SupportedClaims(c),
CodeChallengeMethodsSupported: CodeChallengeMethods(c),
UILocalesSupported: UILocales(c),
} }
} }
@ -58,15 +65,16 @@ func ResponseTypes(c Configuration) []string {
} //TODO: ok for now, check later if dynamic needed } //TODO: ok for now, check later if dynamic needed
} }
func GrantTypes(c Configuration) []string { func GrantTypes(c Configuration) []oidc.GrantType {
grantTypes := []string{ grantTypes := []oidc.GrantType{
string(oidc.GrantTypeCode), oidc.GrantTypeCode,
oidc.GrantTypeImplicit,
} }
if c.GrantTypeTokenExchangeSupported() { if c.GrantTypeTokenExchangeSupported() {
grantTypes = append(grantTypes, string(oidc.GrantTypeTokenExchange)) grantTypes = append(grantTypes, oidc.GrantTypeTokenExchange)
} }
if c.GrantTypeJWTAuthorizationSupported() { if c.GrantTypeJWTAuthorizationSupported() {
grantTypes = append(grantTypes, string(oidc.GrantTypeBearer)) grantTypes = append(grantTypes, oidc.GrantTypeBearer)
} }
return grantTypes return grantTypes
} }
@ -108,7 +116,7 @@ func SubjectTypes(c Configuration) []string {
return []string{"public"} //TODO: config return []string{"public"} //TODO: config
} }
func AuthMethods(c Configuration) []oidc.AuthMethod { func AuthMethodsTokenEndpoint(c Configuration) []oidc.AuthMethod {
authMethods := []oidc.AuthMethod{ authMethods := []oidc.AuthMethod{
oidc.AuthMethodNone, oidc.AuthMethodNone,
oidc.AuthMethodBasic, oidc.AuthMethodBasic,
@ -122,6 +130,16 @@ func AuthMethods(c Configuration) []oidc.AuthMethod {
return authMethods return authMethods
} }
func AuthMethodsIntrospectionEndpoint(c Configuration) []oidc.AuthMethod {
authMethods := []oidc.AuthMethod{
oidc.AuthMethodBasic,
}
if c.AuthMethodPrivateKeyJWTSupported() {
authMethods = append(authMethods, oidc.AuthMethodPrivateKeyJWT)
}
return authMethods
}
func CodeChallengeMethods(c Configuration) []oidc.CodeChallengeMethod { func CodeChallengeMethods(c Configuration) []oidc.CodeChallengeMethod {
codeMethods := make([]oidc.CodeChallengeMethod, 0, 1) codeMethods := make([]oidc.CodeChallengeMethod, 0, 1)
if c.CodeMethodS256Supported() { if c.CodeMethodS256Supported() {
@ -129,3 +147,10 @@ func CodeChallengeMethods(c Configuration) []oidc.CodeChallengeMethod {
} }
return codeMethods return codeMethods
} }
func UILocales(c Configuration) []language.Tag {
return []language.Tag{
language.English,
language.German,
}
}

View file

@ -30,7 +30,7 @@ type OPStorage interface {
AuthorizeClientIDSecret(ctx context.Context, clientID, clientSecret string) error AuthorizeClientIDSecret(ctx context.Context, clientID, clientSecret string) error
SetUserinfoFromScopes(ctx context.Context, userinfo oidc.UserInfoSetter, userID, clientID string, scopes []string) error SetUserinfoFromScopes(ctx context.Context, userinfo oidc.UserInfoSetter, userID, clientID string, scopes []string) error
SetUserinfoFromToken(ctx context.Context, userinfo oidc.UserInfoSetter, tokenID, subject, origin string) error SetUserinfoFromToken(ctx context.Context, userinfo oidc.UserInfoSetter, tokenID, subject, origin string) error
SetIntrospectionFromToken(ctx context.Context, userinfo oidc.IntrospectionResponse, tokenID, subject, callerTokenID, callerSubject string) error SetIntrospectionFromToken(ctx context.Context, userinfo oidc.IntrospectionResponse, tokenID, subject, clientID string) error
GetPrivateClaimsFromScopes(ctx context.Context, userID, clientID string, scopes []string) (map[string]interface{}, error) GetPrivateClaimsFromScopes(ctx context.Context, userID, clientID string, scopes []string) (map[string]interface{}, error)
GetKeyByIDAndUserID(ctx context.Context, keyID, userID string) (*jose.JSONWebKey, error) GetKeyByIDAndUserID(ctx context.Context, keyID, userID string) (*jose.JSONWebKey, error)
ValidateJWTProfileScopes(ctx context.Context, userID string, scope oidc.Scopes) (oidc.Scopes, error) ValidateJWTProfileScopes(ctx context.Context, userID string, scope oidc.Scopes) (oidc.Scopes, error)

View file

@ -3,7 +3,6 @@ package op
import ( import (
"errors" "errors"
"net/http" "net/http"
"strings"
"github.com/caos/oidc/pkg/oidc" "github.com/caos/oidc/pkg/oidc"
"github.com/caos/oidc/pkg/utils" "github.com/caos/oidc/pkg/utils"
@ -16,6 +15,11 @@ type Introspector interface {
AccessTokenVerifier() AccessTokenVerifier AccessTokenVerifier() AccessTokenVerifier
} }
type IntrospectorJWTProfile interface {
Introspector
JWTProfileVerifier() JWTProfileVerifier
}
func introspectionHandler(introspector Introspector) func(http.ResponseWriter, *http.Request) { func introspectionHandler(introspector Introspector) func(http.ResponseWriter, *http.Request) {
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
Introspect(w, r, introspector) Introspect(w, r, introspector)
@ -23,24 +27,18 @@ func introspectionHandler(introspector Introspector) func(http.ResponseWriter, *
} }
func Introspect(w http.ResponseWriter, r *http.Request, introspector Introspector) { func Introspect(w http.ResponseWriter, r *http.Request, introspector Introspector) {
callerToken := r.Header.Get("authorization")
response := oidc.NewIntrospectionResponse() response := oidc.NewIntrospectionResponse()
callerToken, callerSubject, ok := getTokenIDAndSubject(r.Context(), introspector, strings.TrimPrefix(callerToken, oidc.PrefixBearer)) token, clientID, err := ParseTokenIntrospectionRequest(r, introspector)
if !ok {
utils.MarshalJSON(w, response)
return
}
introspectionToken, err := ParseTokenInrospectionRequest(r, introspector.Decoder())
if err != nil { if err != nil {
utils.MarshalJSON(w, response) http.Error(w, err.Error(), http.StatusUnauthorized)
return return
} }
tokenID, subject, ok := getTokenIDAndSubject(r.Context(), introspector, introspectionToken) tokenID, subject, ok := getTokenIDAndSubject(r.Context(), introspector, token)
if !ok { if !ok {
utils.MarshalJSON(w, response) utils.MarshalJSON(w, response)
return return
} }
err = introspector.Storage().SetIntrospectionFromToken(r.Context(), response, tokenID, subject, callerToken, callerSubject) err = introspector.Storage().SetIntrospectionFromToken(r.Context(), response, tokenID, subject, clientID)
if err != nil { if err != nil {
utils.MarshalJSON(w, response) utils.MarshalJSON(w, response)
return return
@ -49,15 +47,31 @@ func Introspect(w http.ResponseWriter, r *http.Request, introspector Introspecto
utils.MarshalJSON(w, response) utils.MarshalJSON(w, response)
} }
func ParseTokenInrospectionRequest(r *http.Request, decoder utils.Decoder) (string, error) { func ParseTokenIntrospectionRequest(r *http.Request, introspector Introspector) (token, clientID string, err error) {
err := r.ParseForm() err = r.ParseForm()
if err != nil { if err != nil {
return "", errors.New("unable to parse request") return "", "", errors.New("unable to parse request")
} }
req := new(oidc.IntrospectionRequest) req := new(struct {
err = decoder.Decode(req, r.Form) oidc.IntrospectionRequest
oidc.ClientAssertionParams
})
err = introspector.Decoder().Decode(req, r.Form)
if err != nil { if err != nil {
return "", errors.New("unable to parse request") return "", "", errors.New("unable to parse request")
} }
return req.Token, nil if introspectorJWTProfile, ok := introspector.(IntrospectorJWTProfile); ok && req.ClientAssertion != "" {
profile, err := VerifyJWTAssertion(r.Context(), req.ClientAssertion, introspectorJWTProfile.JWTProfileVerifier())
if err == nil {
return req.Token, profile.Issuer, nil
}
}
clientID, clientSecret, ok := r.BasicAuth()
if ok {
if err := introspector.Storage().AuthorizeClientIDSecret(r.Context(), clientID, clientSecret); err != nil {
return "", "", err
}
return req.Token, clientID, nil
}
return "", "", errors.New("invalid authorization")
} }

View file

@ -24,7 +24,7 @@ func userinfoHandler(userinfoProvider UserinfoProvider) func(http.ResponseWriter
} }
func Userinfo(w http.ResponseWriter, r *http.Request, userinfoProvider UserinfoProvider) { func Userinfo(w http.ResponseWriter, r *http.Request, userinfoProvider UserinfoProvider) {
accessToken, err := getAccessToken(r, userinfoProvider.Decoder()) accessToken, err := ParseUserinfoRequest(r, userinfoProvider.Decoder())
if err != nil { if err != nil {
http.Error(w, "access token missing", http.StatusUnauthorized) http.Error(w, "access token missing", http.StatusUnauthorized)
return return
@ -43,16 +43,12 @@ func Userinfo(w http.ResponseWriter, r *http.Request, userinfoProvider UserinfoP
utils.MarshalJSON(w, info) utils.MarshalJSON(w, info)
} }
func getAccessToken(r *http.Request, decoder utils.Decoder) (string, error) { func ParseUserinfoRequest(r *http.Request, decoder utils.Decoder) (string, error) {
authHeader := r.Header.Get("authorization") accessToken, err := getAccessToken(r)
if authHeader != "" { if err == nil {
parts := strings.Split(authHeader, "Bearer ") return accessToken, nil
if len(parts) != 2 {
return "", errors.New("invalid auth header")
}
return parts[1], nil
} }
err := r.ParseForm() err = r.ParseForm()
if err != nil { if err != nil {
return "", errors.New("unable to parse request") return "", errors.New("unable to parse request")
} }
@ -64,6 +60,18 @@ func getAccessToken(r *http.Request, decoder utils.Decoder) (string, error) {
return req.AccessToken, nil return req.AccessToken, nil
} }
func getAccessToken(r *http.Request) (string, error) {
authHeader := r.Header.Get("authorization")
if authHeader == "" {
return "", errors.New("no auth header")
}
parts := strings.Split(authHeader, "Bearer ")
if len(parts) != 2 {
return "", errors.New("invalid auth header")
}
return parts[1], nil
}
func getTokenIDAndSubject(ctx context.Context, userinfoProvider UserinfoProvider, accessToken string) (string, string, bool) { func getTokenIDAndSubject(ctx context.Context, userinfoProvider UserinfoProvider, accessToken string) (string, string, bool) {
tokenIDSubject, err := userinfoProvider.Crypto().Decrypt(accessToken) tokenIDSubject, err := userinfoProvider.Crypto().Decrypt(accessToken)
if err == nil { if err == nil {

View file

@ -216,6 +216,14 @@ func WithVerifierOpts(opts ...VerifierOption) Option {
} }
} }
func WithClientKey(path string) Option {
return func(rp *relayingParty) {
config, _ := ConfigFromKeyFile(path)
rp.clientKey = []byte(config.Key)
rp.clientKeyID = config.KeyID
}
}
//Discover calls the discovery endpoint of the provided issuer and returns the found endpoints //Discover calls the discovery endpoint of the provided issuer and returns the found endpoints
func Discover(issuer string, httpClient *http.Client) (Endpoints, error) { func Discover(issuer string, httpClient *http.Client) (Endpoints, error) {
wellKnown := strings.TrimSuffix(issuer, "/") + oidc.DiscoveryEndpoint wellKnown := strings.TrimSuffix(issuer, "/") + oidc.DiscoveryEndpoint
@ -327,14 +335,14 @@ func CodeExchangeHandler(callback func(http.ResponseWriter, *http.Request, *oidc
} }
codeOpts = append(codeOpts, WithCodeVerifier(codeVerifier)) codeOpts = append(codeOpts, WithCodeVerifier(codeVerifier))
} }
//if len(rp.ClientKey()) > 0 { if len(rp.ClientKey()) > 0 {
// assertion, err := oidc.NewJWTProfileAssertionStringFromFileData(rp.ClientKey(), []string{rp.OAuthConfig().Endpoint.TokenURL}) assertion, err := oidc.GenerateJWTProfileToken(oidc.NewJWTProfileAssertion(rp.OAuthConfig().ClientID, rp.ClientKeyID(), []string{"http://localhost:50002/oauth/v2"}, rp.ClientKey()))
// if err != nil { if err != nil {
// http.Error(w, "failed to build assertion: "+err.Error(), http.StatusUnauthorized) http.Error(w, "failed to build assertion: "+err.Error(), http.StatusUnauthorized)
// return return
// } }
// codeOpts = append(codeOpts, WithClientAssertionJWT(assertion)) codeOpts = append(codeOpts, WithClientAssertionJWT(assertion))
//} }
tokens, err := CodeExchange(r.Context(), params.Get("code"), rp, codeOpts...) tokens, err := CodeExchange(r.Context(), params.Get("code"), rp, codeOpts...)
if err != nil { if err != nil {
http.Error(w, "failed to exchange token: "+err.Error(), http.StatusUnauthorized) http.Error(w, "failed to exchange token: "+err.Error(), http.StatusUnauthorized)

View file

@ -4,10 +4,11 @@ import (
"context" "context"
"errors" "errors"
"net/http" "net/http"
"net/url"
"time"
"golang.org/x/oauth2" "golang.org/x/oauth2"
"golang.org/x/oauth2/clientcredentials" "golang.org/x/oauth2/clientcredentials"
"golang.org/x/oauth2/jwt"
"github.com/caos/oidc/pkg/oidc" "github.com/caos/oidc/pkg/oidc"
"github.com/caos/oidc/pkg/utils" "github.com/caos/oidc/pkg/utils"
@ -16,6 +17,7 @@ import (
type ResourceServer interface { type ResourceServer interface {
IntrospectionURL() string IntrospectionURL() string
HttpClient() *http.Client HttpClient() *http.Client
AuthFn() interface{}
} }
type resourceServer struct { type resourceServer struct {
@ -23,6 +25,32 @@ type resourceServer struct {
tokenURL string tokenURL string
introspectURL string introspectURL string
httpClient *http.Client httpClient *http.Client
authFn interface{}
}
type jwtAccessTokenSource struct {
clientID string
audience []string
PrivateKey []byte
PrivateKeyID string
}
func (j *jwtAccessTokenSource) Token() (*oauth2.Token, error) {
iat := time.Now()
exp := iat.Add(time.Hour)
assertion, err := GenerateJWTProfileToken(&oidc.JWTProfileAssertion{
PrivateKeyID: j.PrivateKeyID,
PrivateKey: j.PrivateKey,
Issuer: j.clientID,
Subject: j.clientID,
Audience: j.audience,
Expiration: oidc.Time(exp),
IssuedAt: oidc.Time(iat),
})
if err != nil {
return nil, err
}
return &oauth2.Token{AccessToken: assertion, TokenType: "Bearer", Expiry: exp}, nil
} }
func (r *resourceServer) IntrospectionURL() string { func (r *resourceServer) IntrospectionURL() string {
@ -33,8 +61,12 @@ func (r *resourceServer) HttpClient() *http.Client {
return r.httpClient return r.httpClient
} }
func (r *resourceServer) AuthFn() interface{} {
return r.authFn
}
func NewResourceServerClientCredentials(issuer, clientID, clientSecret string, option RSOption) (ResourceServer, error) { func NewResourceServerClientCredentials(issuer, clientID, clientSecret string, option RSOption) (ResourceServer, error) {
authorizer := func(tokenURL string) func(ctx context.Context) *http.Client { authorizer := func(tokenURL string) func(context.Context) *http.Client {
return (&clientcredentials.Config{ return (&clientcredentials.Config{
ClientID: clientID, ClientID: clientID,
ClientSecret: clientSecret, ClientSecret: clientSecret,
@ -44,20 +76,52 @@ func NewResourceServerClientCredentials(issuer, clientID, clientSecret string, o
return newResourceServer(issuer, authorizer, option) return newResourceServer(issuer, authorizer, option)
} }
func NewResourceServerJWTProfile(issuer, clientID, keyID string, key []byte, options ...RSOption) (ResourceServer, error) { func NewResourceServerJWTProfile(issuer, clientID, keyID string, key []byte, options ...RSOption) (ResourceServer, error) {
authorizer := func(tokenURL string) func(ctx context.Context) *http.Client { ts := &jwtAccessTokenSource{
return (&jwt.Config{ clientID: clientID,
Email: clientID, PrivateKey: key,
Subject: clientID, PrivateKeyID: keyID,
PrivateKey: key, audience: []string{issuer},
PrivateKeyID: keyID,
Audience: issuer,
TokenURL: tokenURL,
}).Client
} }
//authorizer := func(tokenURL string) func(context.Context) *http.Client {
// return func(ctx context.Context) *http.Client {
// return oauth2.NewClient(ctx, oauth2.ReuseTokenSource(token, ts))
// }
//}
authorizer := utils.FormAuthorization(func(values url.Values) {
token, err := ts.Token()
if err != nil {
//return nil, err
}
values.Set("client_assertion", token.AccessToken)
})
return newResourceServer(issuer, authorizer, options...) return newResourceServer(issuer, authorizer, options...)
} }
func newResourceServer(issuer string, authorizer func(tokenURL string) func(ctx context.Context) *http.Client, options ...RSOption) (*resourceServer, error) { //
//func newResourceServer(issuer string, authorizer func(tokenURL string) func(ctx context.Context) *http.Client, options ...RSOption) (*resourceServer, error) {
// rp := &resourceServer{
// issuer: issuer,
// httpClient: utils.DefaultHTTPClient,
// }
// for _, optFunc := range options {
// optFunc(rp)
// }
// if rp.introspectURL == "" || rp.tokenURL == "" {
// endpoints, err := Discover(rp.issuer, rp.httpClient)
// if err != nil {
// return nil, err
// }
// rp.tokenURL = endpoints.TokenURL
// rp.introspectURL = endpoints.IntrospectURL
// }
// if rp.introspectURL == "" || rp.tokenURL == "" {
// return nil, errors.New("introspectURL and/or tokenURL is empty: please provide with either `WithStaticEndpoints` or a discovery url")
// }
// //rp.httpClient = authorizer(rp.tokenURL)(context.WithValue(context.Background(), oauth2.HTTPClient, rp.HttpClient()))
// return rp, nil
//}
func newResourceServer(issuer string, authorizer interface{}, options ...RSOption) (*resourceServer, error) {
rp := &resourceServer{ rp := &resourceServer{
issuer: issuer, issuer: issuer,
httpClient: utils.DefaultHTTPClient, httpClient: utils.DefaultHTTPClient,
@ -76,7 +140,8 @@ func newResourceServer(issuer string, authorizer func(tokenURL string) func(ctx
if rp.introspectURL == "" || rp.tokenURL == "" { if rp.introspectURL == "" || rp.tokenURL == "" {
return nil, errors.New("introspectURL and/or tokenURL is empty: please provide with either `WithStaticEndpoints` or a discovery url") return nil, errors.New("introspectURL and/or tokenURL is empty: please provide with either `WithStaticEndpoints` or a discovery url")
} }
rp.httpClient = authorizer(rp.tokenURL)(context.WithValue(context.Background(), oauth2.HTTPClient, rp.HttpClient())) //rp.httpClient = authorizer(rp.tokenURL)(context.WithValue(context.Background(), oauth2.HTTPClient, rp.HttpClient()))
rp.authFn = authorizer
return rp, nil return rp, nil
} }
@ -85,6 +150,7 @@ func NewResourceServerFromKeyFile(path string, options ...RSOption) (ResourceSer
if err != nil { if err != nil {
return nil, err return nil, err
} }
c.Issuer = "http://localhost:50002/oauth/v2"
return NewResourceServerJWTProfile(c.Issuer, c.ClientID, c.KeyID, []byte(c.Key), options...) return NewResourceServerJWTProfile(c.Issuer, c.ClientID, c.KeyID, []byte(c.Key), options...)
} }
@ -106,7 +172,7 @@ func WithStaticEndpoints(tokenURL, introspectURL string) RSOption {
} }
func Introspect(ctx context.Context, rp ResourceServer, token string) (oidc.IntrospectionResponse, error) { func Introspect(ctx context.Context, rp ResourceServer, token string) (oidc.IntrospectionResponse, error) {
req, err := utils.FormRequest(rp.IntrospectionURL(), &oidc.IntrospectionRequest{Token: token}, encoder, nil) req, err := utils.FormRequest(rp.IntrospectionURL(), &oidc.IntrospectionRequest{Token: token}, encoder, rp.AuthFn())
if err != nil { if err != nil {
return nil, err return nil, err
} }

View file

@ -42,6 +42,9 @@ func FormRequest(endpoint string, request interface{}, encoder Encoder, authFn i
if fn, ok := authFn.(FormAuthorization); ok { if fn, ok := authFn.(FormAuthorization); ok {
fn(form) fn(form)
} }
if fn, ok := authFn.(func(url.Values)); ok {
fn(form)
}
body := strings.NewReader(form.Encode()) body := strings.NewReader(form.Encode())
req, err := http.NewRequest("POST", endpoint, body) req, err := http.NewRequest("POST", endpoint, body)
if err != nil { if err != nil {