diff --git a/example/client/app/app.go b/example/client/app/app.go index ad00adb..c3bf9e0 100644 --- a/example/client/app/app.go +++ b/example/client/app/app.go @@ -27,6 +27,7 @@ var ( func main() { clientID := os.Getenv("CLIENT_ID") clientSecret := os.Getenv("CLIENT_SECRET") + keyPath := os.Getenv("KEY_PATH") issuer := os.Getenv("ISSUER") port := os.Getenv("PORT") scopes := strings.Split(os.Getenv("SCOPES"), " ") @@ -35,10 +36,19 @@ func main() { redirectURI := fmt.Sprintf("http://localhost:%v%v", port, callbackPath) cookieHandler := utils.NewCookieHandler(key, key, utils.WithUnsecure()) - provider, err := rp.NewRelayingPartyOIDC(issuer, clientID, clientSecret, redirectURI, scopes, - rp.WithPKCE(cookieHandler), - rp.WithVerifierOpts(rp.WithIssuedAtOffset(5*time.Second)), - ) + + options := []rp.Option{ + 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 { logrus.Fatalf("error creating provider %s", err.Error()) } diff --git a/pkg/oidc/discovery.go b/pkg/oidc/discovery.go index 970369c..104078c 100644 --- a/pkg/oidc/discovery.go +++ b/pkg/oidc/discovery.go @@ -1,27 +1,56 @@ package oidc +import ( + "golang.org/x/text/language" +) + const ( DiscoveryEndpoint = "/.well-known/openid-configuration" ) type DiscoveryConfiguration struct { - Issuer string `json:"issuer,omitempty"` - AuthorizationEndpoint string `json:"authorization_endpoint,omitempty"` - TokenEndpoint string `json:"token_endpoint,omitempty"` - IntrospectionEndpoint string `json:"introspection_endpoint,omitempty"` - UserinfoEndpoint string `json:"userinfo_endpoint,omitempty"` - EndSessionEndpoint string `json:"end_session_endpoint,omitempty"` - CheckSessionIframe string `json:"check_session_iframe,omitempty"` - JwksURI string `json:"jwks_uri,omitempty"` - ScopesSupported []string `json:"scopes_supported,omitempty"` - ResponseTypesSupported []string `json:"response_types_supported,omitempty"` - ResponseModesSupported []string `json:"response_modes_supported,omitempty"` - GrantTypesSupported []string `json:"grant_types_supported,omitempty"` - SubjectTypesSupported []string `json:"subject_types_supported,omitempty"` - IDTokenSigningAlgValuesSupported []string `json:"id_token_signing_alg_values_supported,omitempty"` - TokenEndpointAuthMethodsSupported []AuthMethod `json:"token_endpoint_auth_methods_supported,omitempty"` - CodeChallengeMethodsSupported []CodeChallengeMethod `json:"code_challenge_methods_supported,omitempty"` - ClaimsSupported []string `json:"claims_supported,omitempty"` + Issuer string `json:"issuer,omitempty"` + AuthorizationEndpoint string `json:"authorization_endpoint,omitempty"` + TokenEndpoint string `json:"token_endpoint,omitempty"` + IntrospectionEndpoint string `json:"introspection_endpoint,omitempty"` + UserinfoEndpoint string `json:"userinfo_endpoint,omitempty"` + RevocationEndpoint string `json:"revocation_endpoint,omitempty"` + EndSessionEndpoint string `json:"end_session_endpoint,omitempty"` + CheckSessionIframe string `json:"check_session_iframe,omitempty"` + JwksURI string `json:"jwks_uri,omitempty"` + ScopesSupported []string `json:"scopes_supported,omitempty"` + ResponseTypesSupported []string `json:"response_types_supported"` + ResponseModesSupported []string `json:"response_modes_supported,omitempty"` + GrantTypesSupported []GrantType `json:"grant_types_supported,omitempty"` + ACRValuesSupported []string `json:"acr_values_supported,omitempty"` + SubjectTypesSupported []string `json:"subject_types_supported,omitempty"` + IDTokenSigningAlgValuesSupported []string `json:"id_token_signing_alg_values_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 @@ -32,3 +61,7 @@ const ( AuthMethodNone AuthMethod = "none" AuthMethodPrivateKeyJWT AuthMethod = "private_key_jwt" ) + +const ( + GrantTypeImplicit GrantType = "implicit" +) diff --git a/pkg/oidc/introspection.go b/pkg/oidc/introspection.go index 98a8e25..1a66520 100644 --- a/pkg/oidc/introspection.go +++ b/pkg/oidc/introspection.go @@ -12,10 +12,17 @@ type IntrospectionRequest struct { Token string `schema:"token"` } +type ClientAssertionParams struct { + ClientAssertion string `schema:"client_assertion"` + ClientAssertionType string `schema:"client_assertion_type"` +} + type IntrospectionResponse interface { UserInfoSetter SetActive(bool) IsActive() bool + SetScopes(scopes Scope) + SetClientID(id string) } func NewIntrospectionResponse() IntrospectionResponse { @@ -23,19 +30,30 @@ func NewIntrospectionResponse() IntrospectionResponse { } type introspectionResponse struct { - Active bool `json:"active"` - Subject string `json:"sub,omitempty"` + Active bool `json:"active"` + Scope Scope `json:"scope,omitempty"` + ClientID string `json:"client_id,omitempty"` + Subject string `json:"sub,omitempty"` userInfoProfile userInfoEmail userInfoPhone - Address UserInfoAddress `json:"address,omitempty"` - claims map[string]interface{} + Address UserInfoAddress `json:"address,omitempty"` + claims map[string]interface{} } func (u *introspectionResponse) IsActive() bool { 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 { return u.Subject } diff --git a/pkg/oidc/token.go b/pkg/oidc/token.go index 1fc40b3..068e8e6 100644 --- a/pkg/oidc/token.go +++ b/pkg/oidc/token.go @@ -427,7 +427,7 @@ func NewJWTProfileAssertionStringFromFileData(data []byte, audience []string) (s if err != nil { 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) { @@ -473,7 +473,7 @@ func AppendClientIDToAudience(clientID string, audience []string) []string { return append(audience, clientID) } -func generateJWTProfileToken(assertion *JWTProfileAssertion) (string, error) { +func GenerateJWTProfileToken(assertion *JWTProfileAssertion) (string, error) { privateKey, err := bytesToPrivateKey(assertion.PrivateKey) if err != nil { return "", err diff --git a/pkg/oidc/types.go b/pkg/oidc/types.go index 86e5d06..fd496da 100644 --- a/pkg/oidc/types.go +++ b/pkg/oidc/types.go @@ -59,6 +59,7 @@ type Prompt string type ResponseType string type Scopes []string +type Scope []string //TODO: hurst? func (s Scopes) Encode() string { return strings.Join(s, " ") @@ -73,6 +74,19 @@ func (s *Scopes) MarshalText() ([]byte, error) { 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 func (t *Time) UnmarshalJSON(data []byte) error { diff --git a/pkg/oidc/userinfo.go b/pkg/oidc/userinfo.go index 3c77b7b..3a92501 100644 --- a/pkg/oidc/userinfo.go +++ b/pkg/oidc/userinfo.go @@ -6,8 +6,6 @@ import ( "time" "golang.org/x/text/language" - - "github.com/caos/oidc/pkg/utils" ) type UserInfo interface { @@ -351,11 +349,17 @@ func (i *userinfo) MarshalJSON() ([]byte, error) { return b, nil } - claims, err := json.Marshal(i.claims) + err = json.Unmarshal(b, &i.claims) if err != nil { 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 { @@ -372,6 +376,10 @@ func (i *userinfo) UnmarshalJSON(data []byte) error { i.UpdatedAt = Time(time.Unix(a.UpdatedAt, 0).UTC()) + if err := json.Unmarshal(data, &i.claims); err != nil { + return err + } + return nil } diff --git a/pkg/op/discovery.go b/pkg/op/discovery.go index 2ac4833..291214c 100644 --- a/pkg/op/discovery.go +++ b/pkg/op/discovery.go @@ -3,6 +3,8 @@ package op import ( "net/http" + "golang.org/x/text/language" + "github.com/caos/oidc/pkg/oidc" "github.com/caos/oidc/pkg/utils" ) @@ -24,17 +26,22 @@ func CreateDiscoveryConfig(c Configuration, s Signer) *oidc.DiscoveryConfigurati TokenEndpoint: c.TokenEndpoint().Absolute(c.Issuer()), IntrospectionEndpoint: c.IntrospectionEndpoint().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), - JwksURI: c.KeysEndpoint().Absolute(c.Issuer()), - ScopesSupported: Scopes(c), - ResponseTypesSupported: ResponseTypes(c), - GrantTypesSupported: GrantTypes(c), - ClaimsSupported: SupportedClaims(c), - IDTokenSigningAlgValuesSupported: SigAlgorithms(s), - SubjectTypesSupported: SubjectTypes(c), - TokenEndpointAuthMethodsSupported: AuthMethods(c), - CodeChallengeMethodsSupported: CodeChallengeMethods(c), + JwksURI: c.KeysEndpoint().Absolute(c.Issuer()), + ScopesSupported: Scopes(c), + ResponseTypesSupported: ResponseTypes(c), + //ResponseModesSupported: + GrantTypesSupported: GrantTypes(c), + //ACRValuesSupported: ACRValues(c), + SubjectTypesSupported: SubjectTypes(c), + IDTokenSigningAlgValuesSupported: SigAlgorithms(s), + 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 } -func GrantTypes(c Configuration) []string { - grantTypes := []string{ - string(oidc.GrantTypeCode), +func GrantTypes(c Configuration) []oidc.GrantType { + grantTypes := []oidc.GrantType{ + oidc.GrantTypeCode, + oidc.GrantTypeImplicit, } if c.GrantTypeTokenExchangeSupported() { - grantTypes = append(grantTypes, string(oidc.GrantTypeTokenExchange)) + grantTypes = append(grantTypes, oidc.GrantTypeTokenExchange) } if c.GrantTypeJWTAuthorizationSupported() { - grantTypes = append(grantTypes, string(oidc.GrantTypeBearer)) + grantTypes = append(grantTypes, oidc.GrantTypeBearer) } return grantTypes } @@ -108,7 +116,7 @@ func SubjectTypes(c Configuration) []string { return []string{"public"} //TODO: config } -func AuthMethods(c Configuration) []oidc.AuthMethod { +func AuthMethodsTokenEndpoint(c Configuration) []oidc.AuthMethod { authMethods := []oidc.AuthMethod{ oidc.AuthMethodNone, oidc.AuthMethodBasic, @@ -122,6 +130,16 @@ func AuthMethods(c Configuration) []oidc.AuthMethod { 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 { codeMethods := make([]oidc.CodeChallengeMethod, 0, 1) if c.CodeMethodS256Supported() { @@ -129,3 +147,10 @@ func CodeChallengeMethods(c Configuration) []oidc.CodeChallengeMethod { } return codeMethods } + +func UILocales(c Configuration) []language.Tag { + return []language.Tag{ + language.English, + language.German, + } +} diff --git a/pkg/op/storage.go b/pkg/op/storage.go index 4072630..967a24c 100644 --- a/pkg/op/storage.go +++ b/pkg/op/storage.go @@ -30,7 +30,7 @@ type OPStorage interface { AuthorizeClientIDSecret(ctx context.Context, clientID, clientSecret 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 - 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) GetKeyByIDAndUserID(ctx context.Context, keyID, userID string) (*jose.JSONWebKey, error) ValidateJWTProfileScopes(ctx context.Context, userID string, scope oidc.Scopes) (oidc.Scopes, error) diff --git a/pkg/op/token_intospection.go b/pkg/op/token_intospection.go index 8ea4dbb..30d2544 100644 --- a/pkg/op/token_intospection.go +++ b/pkg/op/token_intospection.go @@ -3,7 +3,6 @@ package op import ( "errors" "net/http" - "strings" "github.com/caos/oidc/pkg/oidc" "github.com/caos/oidc/pkg/utils" @@ -16,6 +15,11 @@ type Introspector interface { AccessTokenVerifier() AccessTokenVerifier } +type IntrospectorJWTProfile interface { + Introspector + JWTProfileVerifier() JWTProfileVerifier +} + func introspectionHandler(introspector Introspector) func(http.ResponseWriter, *http.Request) { return func(w http.ResponseWriter, r *http.Request) { 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) { - callerToken := r.Header.Get("authorization") response := oidc.NewIntrospectionResponse() - callerToken, callerSubject, ok := getTokenIDAndSubject(r.Context(), introspector, strings.TrimPrefix(callerToken, oidc.PrefixBearer)) - if !ok { - utils.MarshalJSON(w, response) - return - } - introspectionToken, err := ParseTokenInrospectionRequest(r, introspector.Decoder()) + token, clientID, err := ParseTokenIntrospectionRequest(r, introspector) if err != nil { - utils.MarshalJSON(w, response) + http.Error(w, err.Error(), http.StatusUnauthorized) return } - tokenID, subject, ok := getTokenIDAndSubject(r.Context(), introspector, introspectionToken) + tokenID, subject, ok := getTokenIDAndSubject(r.Context(), introspector, token) if !ok { utils.MarshalJSON(w, response) 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 { utils.MarshalJSON(w, response) return @@ -49,15 +47,31 @@ func Introspect(w http.ResponseWriter, r *http.Request, introspector Introspecto utils.MarshalJSON(w, response) } -func ParseTokenInrospectionRequest(r *http.Request, decoder utils.Decoder) (string, error) { - err := r.ParseForm() +func ParseTokenIntrospectionRequest(r *http.Request, introspector Introspector) (token, clientID string, err error) { + err = r.ParseForm() if err != nil { - return "", errors.New("unable to parse request") + return "", "", errors.New("unable to parse request") } - req := new(oidc.IntrospectionRequest) - err = decoder.Decode(req, r.Form) + req := new(struct { + oidc.IntrospectionRequest + oidc.ClientAssertionParams + }) + err = introspector.Decoder().Decode(req, r.Form) 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") } diff --git a/pkg/op/userinfo.go b/pkg/op/userinfo.go index 1163598..d951136 100644 --- a/pkg/op/userinfo.go +++ b/pkg/op/userinfo.go @@ -24,7 +24,7 @@ func userinfoHandler(userinfoProvider UserinfoProvider) func(http.ResponseWriter } 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 { http.Error(w, "access token missing", http.StatusUnauthorized) return @@ -43,16 +43,12 @@ func Userinfo(w http.ResponseWriter, r *http.Request, userinfoProvider UserinfoP utils.MarshalJSON(w, info) } -func getAccessToken(r *http.Request, decoder utils.Decoder) (string, error) { - authHeader := r.Header.Get("authorization") - if authHeader != "" { - parts := strings.Split(authHeader, "Bearer ") - if len(parts) != 2 { - return "", errors.New("invalid auth header") - } - return parts[1], nil +func ParseUserinfoRequest(r *http.Request, decoder utils.Decoder) (string, error) { + accessToken, err := getAccessToken(r) + if err == nil { + return accessToken, nil } - err := r.ParseForm() + err = r.ParseForm() if err != nil { 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 } +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) { tokenIDSubject, err := userinfoProvider.Crypto().Decrypt(accessToken) if err == nil { diff --git a/pkg/rp/relaying_party.go b/pkg/rp/relaying_party.go index be62762..3260657 100644 --- a/pkg/rp/relaying_party.go +++ b/pkg/rp/relaying_party.go @@ -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 func Discover(issuer string, httpClient *http.Client) (Endpoints, error) { wellKnown := strings.TrimSuffix(issuer, "/") + oidc.DiscoveryEndpoint @@ -327,14 +335,14 @@ func CodeExchangeHandler(callback func(http.ResponseWriter, *http.Request, *oidc } codeOpts = append(codeOpts, WithCodeVerifier(codeVerifier)) } - //if len(rp.ClientKey()) > 0 { - // assertion, err := oidc.NewJWTProfileAssertionStringFromFileData(rp.ClientKey(), []string{rp.OAuthConfig().Endpoint.TokenURL}) - // if err != nil { - // http.Error(w, "failed to build assertion: "+err.Error(), http.StatusUnauthorized) - // return - // } - // codeOpts = append(codeOpts, WithClientAssertionJWT(assertion)) - //} + if len(rp.ClientKey()) > 0 { + assertion, err := oidc.GenerateJWTProfileToken(oidc.NewJWTProfileAssertion(rp.OAuthConfig().ClientID, rp.ClientKeyID(), []string{"http://localhost:50002/oauth/v2"}, rp.ClientKey())) + if err != nil { + http.Error(w, "failed to build assertion: "+err.Error(), http.StatusUnauthorized) + return + } + codeOpts = append(codeOpts, WithClientAssertionJWT(assertion)) + } tokens, err := CodeExchange(r.Context(), params.Get("code"), rp, codeOpts...) if err != nil { http.Error(w, "failed to exchange token: "+err.Error(), http.StatusUnauthorized) diff --git a/pkg/rp/resource_server.go b/pkg/rp/resource_server.go index e0488f1..c59097d 100644 --- a/pkg/rp/resource_server.go +++ b/pkg/rp/resource_server.go @@ -4,10 +4,11 @@ import ( "context" "errors" "net/http" + "net/url" + "time" "golang.org/x/oauth2" "golang.org/x/oauth2/clientcredentials" - "golang.org/x/oauth2/jwt" "github.com/caos/oidc/pkg/oidc" "github.com/caos/oidc/pkg/utils" @@ -16,6 +17,7 @@ import ( type ResourceServer interface { IntrospectionURL() string HttpClient() *http.Client + AuthFn() interface{} } type resourceServer struct { @@ -23,6 +25,32 @@ type resourceServer struct { tokenURL string introspectURL string 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 { @@ -33,8 +61,12 @@ func (r *resourceServer) HttpClient() *http.Client { return r.httpClient } +func (r *resourceServer) AuthFn() interface{} { + return r.authFn +} + 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{ ClientID: clientID, ClientSecret: clientSecret, @@ -44,20 +76,52 @@ func NewResourceServerClientCredentials(issuer, clientID, clientSecret string, o return newResourceServer(issuer, authorizer, option) } func NewResourceServerJWTProfile(issuer, clientID, keyID string, key []byte, options ...RSOption) (ResourceServer, error) { - authorizer := func(tokenURL string) func(ctx context.Context) *http.Client { - return (&jwt.Config{ - Email: clientID, - Subject: clientID, - PrivateKey: key, - PrivateKeyID: keyID, - Audience: issuer, - TokenURL: tokenURL, - }).Client + ts := &jwtAccessTokenSource{ + clientID: clientID, + PrivateKey: key, + PrivateKeyID: keyID, + audience: []string{issuer}, } + + //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...) } -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{ issuer: issuer, httpClient: utils.DefaultHTTPClient, @@ -76,7 +140,8 @@ func newResourceServer(issuer string, authorizer func(tokenURL string) func(ctx 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())) + //rp.httpClient = authorizer(rp.tokenURL)(context.WithValue(context.Background(), oauth2.HTTPClient, rp.HttpClient())) + rp.authFn = authorizer return rp, nil } @@ -85,6 +150,7 @@ func NewResourceServerFromKeyFile(path string, options ...RSOption) (ResourceSer if err != nil { return nil, err } + c.Issuer = "http://localhost:50002/oauth/v2" 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) { - 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 { return nil, err } diff --git a/pkg/utils/http.go b/pkg/utils/http.go index fa51815..6f1b74d 100644 --- a/pkg/utils/http.go +++ b/pkg/utils/http.go @@ -42,6 +42,9 @@ func FormRequest(endpoint string, request interface{}, encoder Encoder, authFn i if fn, ok := authFn.(FormAuthorization); ok { fn(form) } + if fn, ok := authFn.(func(url.Values)); ok { + fn(form) + } body := strings.NewReader(form.Encode()) req, err := http.NewRequest("POST", endpoint, body) if err != nil {