diff --git a/example/client/api/api.go b/example/client/api/api.go index 6e1b0bd..e5345a8 100644 --- a/example/client/api/api.go +++ b/example/client/api/api.go @@ -1,90 +1,102 @@ package main -// import ( -// "encoding/json" -// "fmt" -// "log" -// "net/http" -// "os" +import ( + "encoding/json" + "fmt" + "log" + "net/http" + "os" + "strings" + "time" -// "github.com/caos/oidc/pkg/oidc" -// "github.com/caos/oidc/pkg/oidc/rp" -// "github.com/caos/utils/logging" -// ) + "github.com/gorilla/mux" + "github.com/sirupsen/logrus" -// const ( -// publicURL string = "/public" -// protectedURL string = "/protected" -// protectedExchangeURL string = "/protected/exchange" -// ) + "github.com/caos/oidc/pkg/oidc" + "github.com/caos/oidc/pkg/rp" +) + +const ( + publicURL string = "/public" + protectedURL string = "/protected" + protectedClaimURL string = "/protected/{claim}/{value}" +) func main() { - // clientID := os.Getenv("CLIENT_ID") - // clientSecret := os.Getenv("CLIENT_SECRET") - // issuer := os.Getenv("ISSUER") - // port := os.Getenv("PORT") + keyPath := os.Getenv("KEY") + port := os.Getenv("PORT") - // // ctx := context.Background() + provider, err := rp.NewResourceServerFromKeyFile(keyPath) + if err != nil { + logrus.Fatalf("error creating provider %s", err.Error()) + } - // providerConfig := &oidc.ProviderConfig{ - // ClientID: clientID, - // ClientSecret: clientSecret, - // Issuer: issuer, - // } - // provider, err := rp.NewDefaultProvider(providerConfig) - // logging.Log("APP-nx6PeF").OnError(err).Panic("error creating provider") + router := mux.NewRouter() - // http.HandleFunc(publicURL, func(w http.ResponseWriter, r *http.Request) { - // w.Write([]byte("OK")) - // }) + //public url accessible without any authorization + //will print `OK` and current timestamp + router.HandleFunc(publicURL, func(w http.ResponseWriter, r *http.Request) { + w.Write([]byte("OK " + time.Now().String())) + }) - // http.HandleFunc(protectedURL, func(w http.ResponseWriter, r *http.Request) { - // ok, token := checkToken(w, r) - // if !ok { - // return - // } - // resp, err := provider.Introspect(r.Context(), token) - // if err != nil { - // http.Error(w, err.Error(), http.StatusForbidden) - // return - // } - // data, err := json.Marshal(resp) - // if err != nil { - // http.Error(w, err.Error(), http.StatusInternalServerError) - // return - // } - // w.Write(data) - // }) + //protected url which needs an active token + //will print the result of the introspection endpoint on success + router.HandleFunc(protectedURL, func(w http.ResponseWriter, r *http.Request) { + ok, token := checkToken(w, r) + if !ok { + return + } + resp, err := rp.Introspect(r.Context(), provider, token) + if err != nil { + http.Error(w, err.Error(), http.StatusForbidden) + return + } + data, err := json.Marshal(resp) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + w.Write(data) + }) - // http.HandleFunc(protectedExchangeURL, func(w http.ResponseWriter, r *http.Request) { - // ok, token := checkToken(w, r) - // if !ok { - // return - // } - // tokens, err := provider.DelegationTokenExchange(r.Context(), token, oidc.WithResource([]string{"Test"})) - // if err != nil { - // http.Error(w, "failed to exchange token: "+err.Error(), http.StatusUnauthorized) - // return - // } + //protected url which needs an active token and checks if the response of the introspect endpoint + //contains a requested claim with the required (string) value + //e.g. /protected/username/livio@caos.ch + router.HandleFunc(protectedClaimURL, func(w http.ResponseWriter, r *http.Request) { + ok, token := checkToken(w, r) + if !ok { + return + } + resp, err := rp.Introspect(r.Context(), provider, token) + if err != nil { + http.Error(w, err.Error(), http.StatusForbidden) + return + } + params := mux.Vars(r) + requestedClaim := params["claim"] + requestedValue := params["value"] + value, ok := resp.GetClaim(requestedClaim).(string) + if !ok || value == "" || value != requestedValue { + http.Error(w, "claim does not match", http.StatusForbidden) + return + } + w.Write([]byte("authorized with value " + value)) + }) - // data, err := json.Marshal(tokens) - // if err != nil { - // http.Error(w, err.Error(), http.StatusInternalServerError) - // return - // } - // w.Write(data) - // }) - - // lis := fmt.Sprintf("127.0.0.1:%s", port) - // log.Printf("listening on http://%s/", lis) - // log.Fatal(http.ListenAndServe(lis, nil)) - // } - - // func checkToken(w http.ResponseWriter, r *http.Request) (bool, string) { - // token := r.Header.Get("authorization") - // if token == "" { - // http.Error(w, "Auth header missing", http.StatusUnauthorized) - // return false, "" - // } - // return true, token + lis := fmt.Sprintf("127.0.0.1:%s", port) + log.Printf("listening on http://%s/", lis) + log.Fatal(http.ListenAndServe(lis, router)) +} + +func checkToken(w http.ResponseWriter, r *http.Request) (bool, string) { + auth := r.Header.Get("authorization") + if auth == "" { + http.Error(w, "auth header missing", http.StatusUnauthorized) + return false, "" + } + if !strings.HasPrefix(auth, oidc.PrefixBearer) { + http.Error(w, "invalid header", http.StatusUnauthorized) + return false, "" + } + return true, strings.TrimPrefix(auth, oidc.PrefixBearer) } diff --git a/pkg/oidc/code_challenge.go b/pkg/oidc/code_challenge.go index 44a0499..9c4c8a3 100644 --- a/pkg/oidc/code_challenge.go +++ b/pkg/oidc/code_challenge.go @@ -24,7 +24,7 @@ func NewSHACodeChallenge(code string) string { func VerifyCodeChallenge(c *CodeChallenge, codeVerifier string) bool { if c == nil { - return false //TODO: ? + return false } if c.Method == CodeChallengeMethodS256 { codeVerifier = NewSHACodeChallenge(codeVerifier) diff --git a/pkg/oidc/discovery.go b/pkg/oidc/discovery.go index 4621a1f..970369c 100644 --- a/pkg/oidc/discovery.go +++ b/pkg/oidc/discovery.go @@ -5,23 +5,23 @@ const ( ) 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 []string `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"` + 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"` } type AuthMethod string diff --git a/pkg/oidc/grants/tokenexchange/tokenexchange.go b/pkg/oidc/grants/tokenexchange/tokenexchange.go index 5cb6e79..02a9808 100644 --- a/pkg/oidc/grants/tokenexchange/tokenexchange.go +++ b/pkg/oidc/grants/tokenexchange/tokenexchange.go @@ -1,9 +1,5 @@ package tokenexchange -import ( - "github.com/caos/oidc/pkg/oidc" -) - const ( AccessTokenType = "urn:ietf:params:oauth:token-type:access_token" RefreshTokenType = "urn:ietf:params:oauth:token-type:refresh_token" @@ -26,22 +22,6 @@ type TokenExchangeRequest struct { requestedTokenType string `schema:"requested_token_type"` } -type JWTProfileRequest struct { - Assertion string `schema:"assertion"` - Scope oidc.Scopes `schema:"scope"` - GrantType oidc.GrantType `schema:"grant_type"` -} - -//ClientCredentialsGrantBasic creates an oauth2 `Client Credentials` Grant -//sneding client_id and client_secret as basic auth header -func NewJWTProfileRequest(assertion string, scopes ...string) *JWTProfileRequest { - return &JWTProfileRequest{ - GrantType: oidc.GrantTypeBearer, - Assertion: assertion, - Scope: scopes, - } -} - func NewTokenExchangeRequest(subjectToken, subjectTokenType string, opts ...TokenExchangeOption) *TokenExchangeRequest { t := &TokenExchangeRequest{ grantType: TokenExchangeGrantType, diff --git a/pkg/oidc/introspection.go b/pkg/oidc/introspection.go index 6414bef..98f3969 100644 --- a/pkg/oidc/introspection.go +++ b/pkg/oidc/introspection.go @@ -251,5 +251,9 @@ func (i *introspectionResponse) 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/oidc/jwt_profile.go b/pkg/oidc/jwt_profile.go new file mode 100644 index 0000000..6969783 --- /dev/null +++ b/pkg/oidc/jwt_profile.go @@ -0,0 +1,18 @@ +package oidc + +type JWTProfileGrantRequest struct { + Assertion string `schema:"assertion"` + Scope Scopes `schema:"scope"` + GrantType GrantType `schema:"grant_type"` +} + +//NewJWTProfileGrantRequest creates an oauth2 `JSON Web Token (JWT) Profile` Grant +//`urn:ietf:params:oauth:grant-type:jwt-bearer` +//sending a self-signed jwt as assertion +func NewJWTProfileGrantRequest(assertion string, scopes ...string) *JWTProfileGrantRequest { + return &JWTProfileGrantRequest{ + GrantType: GrantTypeBearer, + Assertion: assertion, + Scope: scopes, + } +} diff --git a/pkg/oidc/token.go b/pkg/oidc/token.go index ff8f33e..1fc40b3 100644 --- a/pkg/oidc/token.go +++ b/pkg/oidc/token.go @@ -1,7 +1,10 @@ package oidc import ( + "crypto/rsa" + "crypto/x509" "encoding/json" + "encoding/pem" "io/ioutil" "time" @@ -14,6 +17,8 @@ import ( const ( //BearerToken defines the token_type `Bearer`, which is returned in a successful token response BearerToken = "Bearer" + + PrefixBearer = BearerToken + " " ) type Tokens struct { @@ -397,7 +402,7 @@ type AccessTokenResponse struct { type JWTProfileAssertion struct { PrivateKeyID string `json:"-"` PrivateKey []byte `json:"-"` - Issuer string `json:"issuer"` + Issuer string `json:"iss"` Subject string `json:"sub"` Audience Audience `json:"aud"` Expiration Time `json:"exp"` @@ -412,6 +417,19 @@ func NewJWTProfileAssertionFromKeyJSON(filename string, audience []string) (*JWT return NewJWTProfileAssertionFromFileData(data, audience) } +func NewJWTProfileAssertionStringFromFileData(data []byte, audience []string) (string, error) { + keyData := new(struct { + KeyID string `json:"keyId"` + Key string `json:"key"` + UserID string `json:"userId"` + }) + err := json.Unmarshal(data, keyData) + if err != nil { + return "", err + } + return generateJWTProfileToken(NewJWTProfileAssertion(keyData.UserID, keyData.KeyID, audience, []byte(keyData.Key))) +} + func NewJWTProfileAssertionFromFileData(data []byte, audience []string) (*JWTProfileAssertion, error) { keyData := new(struct { KeyID string `json:"keyId"` @@ -454,3 +472,46 @@ func AppendClientIDToAudience(clientID string, audience []string) []string { } return append(audience, clientID) } + +func generateJWTProfileToken(assertion *JWTProfileAssertion) (string, error) { + privateKey, err := bytesToPrivateKey(assertion.PrivateKey) + if err != nil { + return "", err + } + key := jose.SigningKey{ + Algorithm: jose.RS256, + Key: &jose.JSONWebKey{Key: privateKey, KeyID: assertion.PrivateKeyID}, + } + signer, err := jose.NewSigner(key, &jose.SignerOptions{}) + if err != nil { + return "", err + } + + marshalledAssertion, err := json.Marshal(assertion) + if err != nil { + return "", err + } + signedAssertion, err := signer.Sign(marshalledAssertion) + if err != nil { + return "", err + } + return signedAssertion.CompactSerialize() +} + +func bytesToPrivateKey(priv []byte) (*rsa.PrivateKey, error) { + block, _ := pem.Decode(priv) + enc := x509.IsEncryptedPEMBlock(block) + b := block.Bytes + var err error + if enc { + b, err = x509.DecryptPEMBlock(block, nil) + if err != nil { + return nil, err + } + } + key, err := x509.ParsePKCS1PrivateKey(b) + if err != nil { + return nil, err + } + return key, nil +} diff --git a/pkg/op/authrequest.go b/pkg/op/authrequest.go index 9e320f8..3a79b9b 100644 --- a/pkg/op/authrequest.go +++ b/pkg/op/authrequest.go @@ -5,6 +5,7 @@ import ( "fmt" "net/http" "strings" + "time" "github.com/gorilla/mux" @@ -12,6 +13,23 @@ import ( "github.com/caos/oidc/pkg/utils" ) +type AuthRequest interface { + GetID() string + GetACR() string + GetAMR() []string + GetAudience() []string + GetAuthTime() time.Time + GetClientID() string + GetCodeChallenge() *oidc.CodeChallenge + GetNonce() string + GetRedirectURI() string + GetResponseType() oidc.ResponseType + GetScopes() []string + GetState() string + GetSubject() string + Done() bool +} + type Authorizer interface { Storage() Storage Decoder() utils.Decoder diff --git a/pkg/op/discovery.go b/pkg/op/discovery.go index 8708fbb..2ac4833 100644 --- a/pkg/op/discovery.go +++ b/pkg/op/discovery.go @@ -122,10 +122,10 @@ func AuthMethods(c Configuration) []oidc.AuthMethod { return authMethods } -func CodeChallengeMethods(c Configuration) []string { - codeMethods := make([]string, 0, 1) +func CodeChallengeMethods(c Configuration) []oidc.CodeChallengeMethod { + codeMethods := make([]oidc.CodeChallengeMethod, 0, 1) if c.CodeMethodS256Supported() { - codeMethods = append(codeMethods, CodeMethodS256) + codeMethods = append(codeMethods, oidc.CodeChallengeMethodS256) } return codeMethods } diff --git a/pkg/op/discovery_test.go b/pkg/op/discovery_test.go index c14fac4..e479faa 100644 --- a/pkg/op/discovery_test.go +++ b/pkg/op/discovery_test.go @@ -215,7 +215,7 @@ func Test_AuthMethods(t *testing.T) { m.EXPECT().AuthMethodPostSupported().Return(false) return m }()}, - []string{string(op.AuthMethodBasic)}, + []string{string(oidc.AuthMethodBasic)}, }, { "basic and post", @@ -223,7 +223,7 @@ func Test_AuthMethods(t *testing.T) { m.EXPECT().AuthMethodPostSupported().Return(true) return m }()}, - []string{string(op.AuthMethodBasic), string(op.AuthMethodPost)}, + []string{string(oidc.AuthMethodBasic), string(oidc.AuthMethodPost)}, }, } for _, tt := range tests { diff --git a/pkg/op/op.go b/pkg/op/op.go index fa32a23..ee41630 100644 --- a/pkg/op/op.go +++ b/pkg/op/op.go @@ -17,27 +17,27 @@ import ( ) const ( - healthzEndpoint = "/healthz" + healthEndpoint = "/healthz" readinessEndpoint = "/ready" defaultAuthorizationEndpoint = "authorize" - defaulTokenEndpoint = "oauth/token" + defaultTokenEndpoint = "oauth/token" defaultIntrospectEndpoint = "oauth/introspect" defaultUserinfoEndpoint = "userinfo" defaultEndSessionEndpoint = "end_session" defaultKeysEndpoint = "keys" - AuthMethodBasic AuthMethod = "client_secret_basic" - AuthMethodPost AuthMethod = "client_secret_post" - AuthMethodNone AuthMethod = "none" - AuthMethodPrivateKeyJWT AuthMethod = "private_key_jwt" + //AuthMethodBasic AuthMethod = "client_secret_basic" + //AuthMethodPost AuthMethod = "client_secret_post" + //AuthMethodNone AuthMethod = "none" + //AuthMethodPrivateKeyJWT AuthMethod = "private_key_jwt" - CodeMethodS256 = "S256" + //CodeMethodS256 = "S256" ) var ( DefaultEndpoints = &endpoints{ Authorization: NewEndpoint(defaultAuthorizationEndpoint), - Token: NewEndpoint(defaulTokenEndpoint), + Token: NewEndpoint(defaultTokenEndpoint), Introspection: NewEndpoint(defaultIntrospectEndpoint), Userinfo: NewEndpoint(defaultUserinfoEndpoint), EndSession: NewEndpoint(defaultEndSessionEndpoint), @@ -73,7 +73,7 @@ func CreateRouter(o OpenIDProvider, interceptors ...HttpInterceptor) *mux.Router handlers.AllowedHeaders([]string{"authorization", "content-type"}), handlers.AllowedOriginValidator(allowAllOrigins), )) - router.HandleFunc(healthzEndpoint, healthzHandler) + router.HandleFunc(healthEndpoint, healthHandler) router.HandleFunc(readinessEndpoint, readyHandler(o.Probes())) router.HandleFunc(oidc.DiscoveryEndpoint, discoveryHandler(o, o.Signer())) router.Handle(o.AuthorizationEndpoint().Relative(), intercept(authorizeHandler(o))) diff --git a/pkg/op/probes.go b/pkg/op/probes.go index 7dc00a9..c6bb748 100644 --- a/pkg/op/probes.go +++ b/pkg/op/probes.go @@ -10,7 +10,7 @@ import ( type ProbesFn func(context.Context) error -func healthzHandler(w http.ResponseWriter, r *http.Request) { +func healthHandler(w http.ResponseWriter, r *http.Request) { ok(w) } diff --git a/pkg/op/storage.go b/pkg/op/storage.go index b5f1dfe..c4da464 100644 --- a/pkg/op/storage.go +++ b/pkg/op/storage.go @@ -50,23 +50,6 @@ type StorageNotFoundError interface { IsNotFound() } -type AuthRequest interface { - GetID() string - GetACR() string - GetAMR() []string - GetAudience() []string - GetAuthTime() time.Time - GetClientID() string - GetCodeChallenge() *oidc.CodeChallenge - GetNonce() string - GetRedirectURI() string - GetResponseType() oidc.ResponseType - GetScopes() []string - GetState() string - GetSubject() string - Done() bool -} - type EndSessionRequest struct { UserID string Client Client diff --git a/pkg/op/tokenrequest.go b/pkg/op/tokenrequest.go index 0e295a3..1345005 100644 --- a/pkg/op/tokenrequest.go +++ b/pkg/op/tokenrequest.go @@ -7,7 +7,6 @@ import ( "net/url" "github.com/caos/oidc/pkg/oidc" - "github.com/caos/oidc/pkg/oidc/grants/tokenexchange" "github.com/caos/oidc/pkg/utils" ) @@ -203,12 +202,12 @@ func JWTProfile(w http.ResponseWriter, r *http.Request, exchanger JWTAuthorizati utils.MarshalJSON(w, resp) } -func ParseJWTProfileRequest(r *http.Request, decoder utils.Decoder) (*tokenexchange.JWTProfileRequest, error) { +func ParseJWTProfileRequest(r *http.Request, decoder utils.Decoder) (*oidc.JWTProfileGrantRequest, error) { err := r.ParseForm() if err != nil { return nil, ErrInvalidRequest("error parsing form") } - tokenReq := new(tokenexchange.JWTProfileRequest) + tokenReq := new(oidc.JWTProfileGrantRequest) err = decoder.Decode(tokenReq, r.Form) if err != nil { return nil, ErrInvalidRequest("error decoding form") diff --git a/pkg/op/verifier_jwt_profile.go b/pkg/op/verifier_jwt_profile.go index b30bdc5..03d8264 100644 --- a/pkg/op/verifier_jwt_profile.go +++ b/pkg/op/verifier_jwt_profile.go @@ -70,7 +70,7 @@ func VerifyJWTAssertion(ctx context.Context, assertion string, v JWTProfileVerif //TODO: implement delegation (openid core / oauth rfc) } - keySet := &jwtProfileKeySet{v.Storage(), request.Subject} + keySet := &jwtProfileKeySet{v.Storage(), request.Issuer} if err = oidc.CheckSignature(ctx, assertion, payload, request, nil, keySet); err != nil { return nil, err diff --git a/pkg/rp/key.go b/pkg/rp/key.go new file mode 100644 index 0000000..58a92d1 --- /dev/null +++ b/pkg/rp/key.go @@ -0,0 +1,33 @@ +package rp + +import ( + "encoding/json" + "io/ioutil" +) + +const ( + serviceAccountKey = "serviceaccount" + applicationKey = "application" +) + +type keyFile struct { + Type string `json:"type"` // serviceaccount or application + KeyID string `json:"keyId"` + Key string `json:"key"` + Issuer string `json:"issuer"` + ClientID string `json:"clientId"` + //TokenURL string `json:"token_uri"` + //ProjectID string `json:"project_id"` +} + +func ConfigFromKeyFile(path string) (*keyFile, error) { + data, err := ioutil.ReadFile(path) + if err != nil { + return nil, err + } + var f keyFile + if err := json.Unmarshal(data, &f); err != nil { + return nil, err + } + return &f, nil +} diff --git a/pkg/rp/relaying_party.go b/pkg/rp/relaying_party.go index 6807221..be62762 100644 --- a/pkg/rp/relaying_party.go +++ b/pkg/rp/relaying_party.go @@ -53,6 +53,9 @@ type RelayingParty interface { //IsOAuth2Only specifies whether relaying party handles only oauth2 or oidc calls IsOAuth2Only() bool + ClientKey() []byte + ClientKeyID() string + //IDTokenVerifier returns the verifier interface used for oidc id_token verification IDTokenVerifier() IDTokenVerifier @@ -74,11 +77,13 @@ type relayingParty struct { oauthConfig *oauth2.Config oauth2Only bool pkce bool + clientKey []byte + clientKeyID string httpClient *http.Client cookieHandler *utils.CookieHandler - errorHandler func(http.ResponseWriter, *http.Request, string, string, string) + errorHandler func(http.ResponseWriter, *http.Request, string, string, string) idTokenVerifier IDTokenVerifier verifierOpts []VerifierOption } @@ -103,6 +108,14 @@ func (rp *relayingParty) IsOAuth2Only() bool { return rp.oauth2Only } +func (rp *relayingParty) ClientKey() []byte { + return rp.clientKey +} + +func (rp *relayingParty) ClientKeyID() string { + return rp.clientKeyID +} + func (rp *relayingParty) IDTokenVerifier() IDTokenVerifier { if rp.idTokenVerifier == nil { rp.idTokenVerifier = NewIDTokenVerifier(rp.issuer, rp.oauthConfig.ClientID, NewRemoteKeySet(rp.httpClient, rp.endpoints.JKWsURL), rp.verifierOpts...) @@ -314,6 +327,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)) + //} tokens, err := CodeExchange(r.Context(), params.Get("code"), rp, codeOpts...) if err != nil { http.Error(w, "failed to exchange token: "+err.Error(), http.StatusUnauthorized) @@ -439,3 +460,13 @@ func WithCodeVerifier(codeVerifier string) CodeExchangeOpt { return []oauth2.AuthCodeOption{oauth2.SetAuthURLParam("code_verifier", codeVerifier)} } } + +//WithClientAssertionJWT sets the `client_assertion` param in the token request +func WithClientAssertionJWT(clientAssertion string) CodeExchangeOpt { + return func() []oauth2.AuthCodeOption { + return []oauth2.AuthCodeOption{ + oauth2.SetAuthURLParam("client_assertion", clientAssertion), + oauth2.SetAuthURLParam("client_assertion_type", oidc.ClientAssertionTypeJWTAssertion), + } + } +} diff --git a/pkg/rp/resource_server.go b/pkg/rp/resource_server.go new file mode 100644 index 0000000..e0488f1 --- /dev/null +++ b/pkg/rp/resource_server.go @@ -0,0 +1,118 @@ +package rp + +import ( + "context" + "errors" + "net/http" + + "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" +) + +type ResourceServer interface { + IntrospectionURL() string + HttpClient() *http.Client +} + +type resourceServer struct { + issuer string + tokenURL string + introspectURL string + httpClient *http.Client +} + +func (r *resourceServer) IntrospectionURL() string { + return r.introspectURL +} + +func (r *resourceServer) HttpClient() *http.Client { + return r.httpClient +} + +func NewResourceServerClientCredentials(issuer, clientID, clientSecret string, option RSOption) (ResourceServer, error) { + authorizer := func(tokenURL string) func(ctx context.Context) *http.Client { + return (&clientcredentials.Config{ + ClientID: clientID, + ClientSecret: clientSecret, + TokenURL: tokenURL, + }).Client + } + 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 + } + return newResourceServer(issuer, authorizer, options...) +} + +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 NewResourceServerFromKeyFile(path string, options ...RSOption) (ResourceServer, error) { + c, err := ConfigFromKeyFile(path) + if err != nil { + return nil, err + } + return NewResourceServerJWTProfile(c.Issuer, c.ClientID, c.KeyID, []byte(c.Key), options...) +} + +type RSOption func(*resourceServer) + +//WithClient provides the ability to set an http client to be used for the resource server +func WithClient(client *http.Client) RSOption { + return func(server *resourceServer) { + server.httpClient = client + } +} + +//WithStaticEndpoints provides the ability to set static token and introspect URL +func WithStaticEndpoints(tokenURL, introspectURL string) RSOption { + return func(server *resourceServer) { + server.tokenURL = tokenURL + server.introspectURL = introspectURL + } +} + +func Introspect(ctx context.Context, rp ResourceServer, token string) (oidc.IntrospectionResponse, error) { + req, err := utils.FormRequest(rp.IntrospectionURL(), &oidc.IntrospectionRequest{Token: token}, encoder, nil) + if err != nil { + return nil, err + } + resp := oidc.NewIntrospectionResponse() + if err := utils.HttpRequest(rp.HttpClient(), req, resp); err != nil { + return nil, err + } + return resp, nil +} diff --git a/pkg/rp/tockenexchange.go b/pkg/rp/tockenexchange.go index 4396dc4..98bd841 100644 --- a/pkg/rp/tockenexchange.go +++ b/pkg/rp/tockenexchange.go @@ -43,8 +43,8 @@ func DelegationTokenExchange(ctx context.Context, subjectToken string, rp Relayi } //JWTProfileExchange handles the oauth2 jwt profile exchange -func JWTProfileExchange(ctx context.Context, jwtProfileRequest *tokenexchange.JWTProfileRequest, rp RelayingParty) (*oauth2.Token, error) { - return CallTokenEndpoint(jwtProfileRequest, rp) +func JWTProfileExchange(ctx context.Context, jwtProfileGrantRequest *oidc.JWTProfileGrantRequest, rp RelayingParty) (*oauth2.Token, error) { + return CallTokenEndpoint(jwtProfileGrantRequest, rp) } //JWTProfileExchange handles the oauth2 jwt profile exchange @@ -53,7 +53,7 @@ func JWTProfileAssertionExchange(ctx context.Context, assertion *oidc.JWTProfile if err != nil { return nil, err } - return JWTProfileExchange(ctx, tokenexchange.NewJWTProfileRequest(token, scopes...), rp) + return JWTProfileExchange(ctx, oidc.NewJWTProfileGrantRequest(token, scopes...), rp) } func generateJWTProfileToken(assertion *oidc.JWTProfileAssertion) (string, error) {