From 0ca2370d48b15b175141155f37673095de1729a9 Mon Sep 17 00:00:00 2001 From: Livio Amstutz Date: Thu, 11 Feb 2021 17:38:58 +0100 Subject: [PATCH] refactoring --- example/client/api/api.go | 9 +- example/client/app/app.go | 83 +-------- example/client/github/github.go | 10 +- example/client/service/service.go | 196 ++++++++++++++++++++++ pkg/client/client.go | 90 ++++++++++ pkg/client/jwt_profile.go | 30 ++++ pkg/{rp => client}/key.go | 8 +- pkg/client/profile/jwt_profile.go | 85 ++++++++++ pkg/{ => client}/rp/cli/cli.go | 8 +- pkg/{ => client}/rp/delegation.go | 0 pkg/{ => client}/rp/jwks.go | 0 pkg/{ => client}/rp/mock/generate.go | 0 pkg/{ => client}/rp/mock/verifier.mock.go | 10 +- pkg/{ => client}/rp/relaying_party.go | 196 ++++++++++------------ pkg/client/rp/tockenexchange.go | 27 +++ pkg/{ => client}/rp/verifier.go | 10 -- pkg/client/rs/resource_server.go | 123 ++++++++++++++ pkg/oidc/discovery.go | 1 + pkg/op/storage.go | 5 - pkg/op/token.go | 3 +- pkg/op/tokenrequest.go | 3 + pkg/op/userinfo.go | 3 +- pkg/rp/resource_server.go | 184 -------------------- pkg/rp/tockenexchange.go | 100 ----------- pkg/utils/key.go | 25 +++ 25 files changed, 698 insertions(+), 511 deletions(-) create mode 100644 example/client/service/service.go create mode 100644 pkg/client/client.go create mode 100644 pkg/client/jwt_profile.go rename pkg/{rp => client}/key.go (78%) create mode 100644 pkg/client/profile/jwt_profile.go rename pkg/{ => client}/rp/cli/cli.go (67%) rename pkg/{ => client}/rp/delegation.go (100%) rename pkg/{ => client}/rp/jwks.go (100%) rename pkg/{ => client}/rp/mock/generate.go (100%) rename pkg/{ => client}/rp/mock/verifier.mock.go (94%) rename pkg/{ => client}/rp/relaying_party.go (69%) create mode 100644 pkg/client/rp/tockenexchange.go rename pkg/{ => client}/rp/verifier.go (92%) create mode 100644 pkg/client/rs/resource_server.go delete mode 100644 pkg/rp/resource_server.go delete mode 100644 pkg/rp/tockenexchange.go create mode 100644 pkg/utils/key.go diff --git a/example/client/api/api.go b/example/client/api/api.go index e5345a8..a3ae85e 100644 --- a/example/client/api/api.go +++ b/example/client/api/api.go @@ -12,8 +12,8 @@ import ( "github.com/gorilla/mux" "github.com/sirupsen/logrus" + "github.com/caos/oidc/pkg/client/rs" "github.com/caos/oidc/pkg/oidc" - "github.com/caos/oidc/pkg/rp" ) const ( @@ -25,8 +25,9 @@ const ( func main() { keyPath := os.Getenv("KEY") port := os.Getenv("PORT") + issuer := os.Getenv("ISSUER") - provider, err := rp.NewResourceServerFromKeyFile(keyPath) + provider, err := rs.NewResourceServerFromKeyFile(issuer, keyPath) if err != nil { logrus.Fatalf("error creating provider %s", err.Error()) } @@ -46,7 +47,7 @@ func main() { if !ok { return } - resp, err := rp.Introspect(r.Context(), provider, token) + resp, err := rs.Introspect(r.Context(), provider, token) if err != nil { http.Error(w, err.Error(), http.StatusForbidden) return @@ -67,7 +68,7 @@ func main() { if !ok { return } - resp, err := rp.Introspect(r.Context(), provider, token) + resp, err := rs.Introspect(r.Context(), provider, token) if err != nil { http.Error(w, err.Error(), http.StatusForbidden) return diff --git a/example/client/app/app.go b/example/client/app/app.go index c3bf9e0..e3ddd15 100644 --- a/example/client/app/app.go +++ b/example/client/app/app.go @@ -1,11 +1,8 @@ package main import ( - "context" "encoding/json" "fmt" - "html/template" - "io/ioutil" "net/http" "os" "strings" @@ -14,8 +11,8 @@ import ( "github.com/google/uuid" "github.com/sirupsen/logrus" + "github.com/caos/oidc/pkg/client/rp" "github.com/caos/oidc/pkg/oidc" - "github.com/caos/oidc/pkg/rp" "github.com/caos/oidc/pkg/utils" ) @@ -32,8 +29,6 @@ func main() { port := os.Getenv("PORT") scopes := strings.Split(os.Getenv("SCOPES"), " ") - ctx := context.Background() - redirectURI := fmt.Sprintf("http://localhost:%v%v", port, callbackPath) cookieHandler := utils.NewCookieHandler(key, key, utils.WithUnsecure()) @@ -48,7 +43,7 @@ func main() { options = append(options, rp.WithClientKey(keyPath)) } - provider, err := rp.NewRelayingPartyOIDC(issuer, clientID, clientSecret, redirectURI, scopes, options...) + provider, err := rp.NewRelyingPartyOIDC(issuer, clientID, clientSecret, redirectURI, scopes, options...) if err != nil { logrus.Fatalf("error creating provider %s", err.Error()) } @@ -81,80 +76,6 @@ func main() { //with the returned tokens from the token endpoint http.Handle(callbackPath, rp.CodeExchangeHandler(marshal, provider)) - http.HandleFunc("/test", func(w http.ResponseWriter, r *http.Request) { - tokens, err := rp.ClientCredentials(ctx, provider, "scope") - if err != nil { - http.Error(w, "failed to exchange token: "+err.Error(), http.StatusUnauthorized) - return - } - - data, err := json.Marshal(tokens) - if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } - w.Write(data) - }) - - http.HandleFunc("/jwt-profile", func(w http.ResponseWriter, r *http.Request) { - if r.Method == "GET" { - tpl := ` - - - - - Login - - -
- - - -
- - ` - t, err := template.New("login").Parse(tpl) - if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } - err = t.Execute(w, nil) - if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - } - } else { - err := r.ParseMultipartForm(4 << 10) - if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } - file, handler, err := r.FormFile("key") - if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } - defer file.Close() - - key, err := ioutil.ReadAll(file) - fmt.Println(handler.Header) - assertion, err := oidc.NewJWTProfileAssertionFromFileData(key, []string{issuer}) - if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } - token, err := rp.JWTProfileAssertionExchange(ctx, assertion, scopes, provider) - if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } - data, err := json.Marshal(token) - if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } - w.Write(data) - } - }) lis := fmt.Sprintf("127.0.0.1:%s", port) logrus.Infof("listening on http://%s/", lis) logrus.Fatal(http.ListenAndServe("127.0.0.1:"+port, nil)) diff --git a/example/client/github/github.go b/example/client/github/github.go index c136091..f39c40b 100644 --- a/example/client/github/github.go +++ b/example/client/github/github.go @@ -10,8 +10,8 @@ import ( "golang.org/x/oauth2" githubOAuth "golang.org/x/oauth2/github" - "github.com/caos/oidc/pkg/rp" - "github.com/caos/oidc/pkg/rp/cli" + "github.com/caos/oidc/pkg/client/rp" + "github.com/caos/oidc/pkg/client/rp/cli" "github.com/caos/oidc/pkg/utils" ) @@ -35,7 +35,7 @@ func main() { ctx := context.Background() cookieHandler := utils.NewCookieHandler(key, key, utils.WithUnsecure()) - relayingParty, err := rp.NewRelayingPartyOAuth(rpConfig, rp.WithCookieHandler(cookieHandler)) + relyingParty, err := rp.NewRelyingPartyOAuth(rpConfig, rp.WithCookieHandler(cookieHandler)) if err != nil { fmt.Printf("error creating relaying party: %v", err) return @@ -43,9 +43,9 @@ func main() { state := func() string { return uuid.New().String() } - token := cli.CodeFlow(relayingParty, callbackPath, port, state) + token := cli.CodeFlow(relyingParty, callbackPath, port, state) - client := github.NewClient(relayingParty.OAuthConfig().Client(ctx, token.Token)) + client := github.NewClient(relyingParty.OAuthConfig().Client(ctx, token.Token)) _, _, err = client.Users.Get(ctx, "") if err != nil { diff --git a/example/client/service/service.go b/example/client/service/service.go new file mode 100644 index 0000000..95227d0 --- /dev/null +++ b/example/client/service/service.go @@ -0,0 +1,196 @@ +package main + +import ( + "context" + "encoding/json" + "fmt" + "html/template" + "io/ioutil" + "net/http" + "os" + "strings" + + "github.com/sirupsen/logrus" + "golang.org/x/oauth2" + + "github.com/caos/oidc/pkg/client/profile" +) + +var ( + client *http.Client = http.DefaultClient +) + +func main() { + //keyPath := os.Getenv("KEY_PATH") + issuer := os.Getenv("ISSUER") + port := os.Getenv("PORT") + scopes := strings.Split(os.Getenv("SCOPES"), " ") + //testURL := os.Getenv("TEST_URL") + + //if keyPath != "" { + // ts, err := rp.NewJWTProfileTokenSourceFromFile(issuer, keyPath, scopes) + // if err != nil { + // logrus.Fatalf("error creating token source %s", err.Error()) + // } + // //client = oauth2.NewClient(context.Background(), ts) + // resp, err := callExampleEndpoint(client, testURL) + // if err != nil { + // logrus.Fatalf("error response from test url: %s", err.Error()) + // } + // fmt.Println(resp) + //} + + http.HandleFunc("/jwt-profile", func(w http.ResponseWriter, r *http.Request) { + if r.Method == "GET" { + tpl := ` + + + + + Login + + +
+ + + +
+ + ` + t, err := template.New("login").Parse(tpl) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + err = t.Execute(w, nil) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + } + } else { + err := r.ParseMultipartForm(4 << 10) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + file, _, err := r.FormFile("key") + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + defer file.Close() + + key, err := ioutil.ReadAll(file) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + ts, err := profile.NewJWTProfileTokenSourceFromKeyFile(issuer, key, scopes) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + client = oauth2.NewClient(context.Background(), ts) + token, err := ts.Token() + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + //assertion, err := oidc.NewJWTProfileAssertionFromFileData(key, []string{issuer}) + //if err != nil { + // http.Error(w, err.Error(), http.StatusInternalServerError) + // return + //} + //token, err := rp.JWTProfileAssertionExchange(ctx, assertion, scopes, provider) + //if err != nil { + // http.Error(w, err.Error(), http.StatusInternalServerError) + // return + //} + data, err := json.Marshal(token) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + w.Write(data) + } + }) + + http.HandleFunc("/test", func(w http.ResponseWriter, r *http.Request) { + tpl := ` + + + + + Test + + +
+ + + +
+ {{if .URL}} +

+ Result for {{.URL}}: {{.Response}} +

+ {{end}} + + ` + err := r.ParseForm() + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + testURL := r.Form.Get("url") + var data struct { + URL string + Response interface{} + } + if testURL != "" { + data.URL = testURL + data.Response, err = callExampleEndpoint(client, testURL) + if err != nil { + data.Response = err + } + } + t, err := template.New("login").Parse(tpl) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + err = t.Execute(w, data) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + } + + }) + lis := fmt.Sprintf("127.0.0.1:%s", port) + logrus.Infof("listening on http://%s/", lis) + logrus.Fatal(http.ListenAndServe("127.0.0.1:"+port, nil)) +} + +func callExampleEndpoint(client *http.Client, testURL string) (interface{}, error) { + req, err := http.NewRequest("GET", testURL, nil) + if err != nil { + return nil, err + } + + resp, err := client.Do(req) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + return nil, err + } + + if resp.StatusCode != http.StatusOK { + return "", fmt.Errorf("http status not ok: %s %s", resp.Status, body) + } + + if strings.HasPrefix(resp.Header.Get("content-type"), "text/plain") { + return string(body), nil + } + return body, err +} diff --git a/pkg/client/client.go b/pkg/client/client.go new file mode 100644 index 0000000..b2b815e --- /dev/null +++ b/pkg/client/client.go @@ -0,0 +1,90 @@ +package client + +import ( + "net/http" + "reflect" + "strings" + "time" + + "github.com/gorilla/schema" + "golang.org/x/oauth2" + "gopkg.in/square/go-jose.v2" + + "github.com/caos/oidc/pkg/oidc" + "github.com/caos/oidc/pkg/utils" +) + +var ( + Encoder = func() utils.Encoder { + e := schema.NewEncoder() + e.RegisterEncoder(oidc.Scopes{}, func(value reflect.Value) string { + return value.Interface().(oidc.Scopes).Encode() + }) + return e + }() +) + +//Discover calls the discovery endpoint of the provided issuer and returns its configuration +func Discover(issuer string, httpClient *http.Client) (*oidc.DiscoveryConfiguration, error) { + wellKnown := strings.TrimSuffix(issuer, "/") + oidc.DiscoveryEndpoint + req, err := http.NewRequest("GET", wellKnown, nil) + if err != nil { + return nil, err + } + discoveryConfig := new(oidc.DiscoveryConfiguration) + err = utils.HttpRequest(httpClient, req, &discoveryConfig) + if err != nil { + return nil, err + } + return discoveryConfig, nil +} + +type tokenEndpointCaller interface { + TokenEndpoint() string + HttpClient() *http.Client +} + +func CallTokenEndpoint(request interface{}, caller tokenEndpointCaller) (newToken *oauth2.Token, err error) { + return callTokenEndpoint(request, nil, caller) +} + +func callTokenEndpoint(request interface{}, authFn interface{}, caller tokenEndpointCaller) (newToken *oauth2.Token, err error) { + req, err := utils.FormRequest(caller.TokenEndpoint(), request, Encoder, authFn) + if err != nil { + return nil, err + } + tokenRes := new(oidc.AccessTokenResponse) + if err := utils.HttpRequest(caller.HttpClient(), req, &tokenRes); err != nil { + return nil, err + } + return &oauth2.Token{ + AccessToken: tokenRes.AccessToken, + TokenType: tokenRes.TokenType, + RefreshToken: tokenRes.RefreshToken, + Expiry: time.Now().UTC().Add(time.Duration(tokenRes.ExpiresIn) * time.Second), + }, nil +} + +func NewSignerFromPrivateKeyByte(key []byte, keyID string) (jose.Signer, error) { + privateKey, err := utils.BytesToPrivateKey(key) + if err != nil { + return nil, err + } + signingKey := jose.SigningKey{ + Algorithm: jose.RS256, + Key: &jose.JSONWebKey{Key: privateKey, KeyID: keyID}, + } + return jose.NewSigner(signingKey, &jose.SignerOptions{}) +} + +func SignedJWTProfileAssertion(clientID string, audience []string, expiration time.Duration, signer jose.Signer) (string, error) { + iat := time.Now() + exp := iat.Add(expiration) + return utils.Sign(&oidc.JWTTokenRequest{ + Issuer: clientID, + Subject: clientID, + Audience: audience, + ExpiresAt: oidc.Time(exp), + IssuedAt: oidc.Time(iat), + }, signer) +} diff --git a/pkg/client/jwt_profile.go b/pkg/client/jwt_profile.go new file mode 100644 index 0000000..8095588 --- /dev/null +++ b/pkg/client/jwt_profile.go @@ -0,0 +1,30 @@ +package client + +import ( + "context" + "net/url" + + "golang.org/x/oauth2" + + "github.com/caos/oidc/pkg/oidc" + "github.com/caos/oidc/pkg/utils" +) + +//JWTProfileExchange handles the oauth2 jwt profile exchange +func JWTProfileExchange(ctx context.Context, jwtProfileGrantRequest *oidc.JWTProfileGrantRequest, caller tokenEndpointCaller) (*oauth2.Token, error) { + return CallTokenEndpoint(jwtProfileGrantRequest, caller) +} + +func ClientAssertionCodeOptions(assertion string) []oauth2.AuthCodeOption { + return []oauth2.AuthCodeOption{ + oauth2.SetAuthURLParam("client_assertion", assertion), + oauth2.SetAuthURLParam("client_assertion_type", oidc.ClientAssertionTypeJWTAssertion), + } +} + +func ClientAssertionFormAuthorization(assertion string) utils.FormAuthorization { + return func(values url.Values) { + values.Set("client_assertion", assertion) + values.Set("client_assertion_type", oidc.ClientAssertionTypeJWTAssertion) + } +} diff --git a/pkg/rp/key.go b/pkg/client/key.go similarity index 78% rename from pkg/rp/key.go rename to pkg/client/key.go index 26d8bf5..f89a2b4 100644 --- a/pkg/rp/key.go +++ b/pkg/client/key.go @@ -1,4 +1,4 @@ -package rp +package client import ( "encoding/json" @@ -14,7 +14,7 @@ type keyFile struct { Type string `json:"type"` // serviceaccount or application KeyID string `json:"keyId"` Key string `json:"key"` - Issuer string `json:"issuer"` + Issuer string `json:"issuer"` //not yet in file //serviceaccount UserID string `json:"userId"` @@ -28,6 +28,10 @@ func ConfigFromKeyFile(path string) (*keyFile, error) { if err != nil { return nil, err } + return ConfigFromKeyFileData(data) +} + +func ConfigFromKeyFileData(data []byte) (*keyFile, error) { var f keyFile if err := json.Unmarshal(data, &f); err != nil { return nil, err diff --git a/pkg/client/profile/jwt_profile.go b/pkg/client/profile/jwt_profile.go new file mode 100644 index 0000000..d60a2f8 --- /dev/null +++ b/pkg/client/profile/jwt_profile.go @@ -0,0 +1,85 @@ +package profile + +import ( + "net/http" + "time" + + "golang.org/x/oauth2" + "gopkg.in/square/go-jose.v2" + + "github.com/caos/oidc/pkg/client" + "github.com/caos/oidc/pkg/oidc" +) + +//jwtProfileTokenSource implement the oauth2.TokenSource +//it will request a token using the OAuth2 JWT Profile Grant +//therefore sending an `assertion` by singing a JWT with the provided private key +type jwtProfileTokenSource struct { + clientID string + audience []string + signer jose.Signer + scopes []string + httpClient *http.Client + tokenEndpoint string +} + +func NewJWTProfileTokenSourceFromKeyFile(issuer string, data []byte, scopes []string, options ...func(source *jwtProfileTokenSource)) (oauth2.TokenSource, error) { + keyData, err := client.ConfigFromKeyFileData(data) + if err != nil { + return nil, err + } + return NewJWTProfileTokenSource(issuer, keyData.UserID, keyData.KeyID, []byte(keyData.Key), scopes, options...) +} + +func NewJWTProfileTokenSource(issuer, clientID, keyID string, key []byte, scopes []string, options ...func(source *jwtProfileTokenSource)) (oauth2.TokenSource, error) { + signer, err := client.NewSignerFromPrivateKeyByte(key, keyID) + if err != nil { + return nil, err + } + source := &jwtProfileTokenSource{ + clientID: clientID, + audience: []string{issuer}, + signer: signer, + scopes: scopes, + httpClient: http.DefaultClient, + } + for _, opt := range options { + opt(source) + } + if source.tokenEndpoint == "" { + config, err := client.Discover(issuer, source.httpClient) + if err != nil { + return nil, err + } + source.tokenEndpoint = config.TokenEndpoint + } + return source, nil +} + +func WithHTTPClient(client *http.Client) func(*jwtProfileTokenSource) { + return func(source *jwtProfileTokenSource) { + source.httpClient = client + } +} + +func WithStaticTokenEndpoint(issuer, tokenEndpoint string) func(*jwtProfileTokenSource) { + return func(source *jwtProfileTokenSource) { + source.tokenEndpoint = tokenEndpoint + } +} + +func (j *jwtProfileTokenSource) TokenEndpoint() string { + return j.tokenEndpoint +} + +func (j *jwtProfileTokenSource) HttpClient() *http.Client { + return j.httpClient +} + +func (j *jwtProfileTokenSource) Token() (*oauth2.Token, error) { + assertion, err := client.SignedJWTProfileAssertion(j.clientID, j.audience, time.Hour, j.signer) + if err != nil { + return nil, err + } + return client.JWTProfileExchange(nil, oidc.NewJWTProfileGrantRequest(assertion, j.scopes...), j) +} diff --git a/pkg/rp/cli/cli.go b/pkg/client/rp/cli/cli.go similarity index 67% rename from pkg/rp/cli/cli.go rename to pkg/client/rp/cli/cli.go index 4b00ba0..6cbb364 100644 --- a/pkg/rp/cli/cli.go +++ b/pkg/client/rp/cli/cli.go @@ -4,8 +4,8 @@ import ( "context" "net/http" + "github.com/caos/oidc/pkg/client/rp" "github.com/caos/oidc/pkg/oidc" - "github.com/caos/oidc/pkg/rp" "github.com/caos/oidc/pkg/utils" ) @@ -13,7 +13,7 @@ const ( loginPath = "/login" ) -func CodeFlow(relayingParty rp.RelayingParty, callbackPath, port string, stateProvider func() string) *oidc.Tokens { +func CodeFlow(relyingParty rp.RelyingParty, callbackPath, port string, stateProvider func() string) *oidc.Tokens { ctx, cancel := context.WithCancel(context.Background()) defer cancel() @@ -24,8 +24,8 @@ func CodeFlow(relayingParty rp.RelayingParty, callbackPath, port string, statePr msg = msg + "

You are authenticated and can now return to the CLI.

" w.Write([]byte(msg)) } - http.Handle(loginPath, rp.AuthURLHandler(stateProvider, relayingParty)) - http.Handle(callbackPath, rp.CodeExchangeHandler(callback, relayingParty)) + http.Handle(loginPath, rp.AuthURLHandler(stateProvider, relyingParty)) + http.Handle(callbackPath, rp.CodeExchangeHandler(callback, relyingParty)) utils.StartServer(ctx, port) diff --git a/pkg/rp/delegation.go b/pkg/client/rp/delegation.go similarity index 100% rename from pkg/rp/delegation.go rename to pkg/client/rp/delegation.go diff --git a/pkg/rp/jwks.go b/pkg/client/rp/jwks.go similarity index 100% rename from pkg/rp/jwks.go rename to pkg/client/rp/jwks.go diff --git a/pkg/rp/mock/generate.go b/pkg/client/rp/mock/generate.go similarity index 100% rename from pkg/rp/mock/generate.go rename to pkg/client/rp/mock/generate.go diff --git a/pkg/rp/mock/verifier.mock.go b/pkg/client/rp/mock/verifier.mock.go similarity index 94% rename from pkg/rp/mock/verifier.mock.go rename to pkg/client/rp/mock/verifier.mock.go index acd7d77..08cf77f 100644 --- a/pkg/rp/mock/verifier.mock.go +++ b/pkg/client/rp/mock/verifier.mock.go @@ -5,10 +5,12 @@ package mock import ( - context "context" - oidc "github.com/caos/oidc/pkg/oidc" - gomock "github.com/golang/mock/gomock" - reflect "reflect" + "context" + "reflect" + + "github.com/golang/mock/gomock" + + "github.com/caos/oidc/pkg/oidc" ) // MockVerifier is a mock of Verifier interface diff --git a/pkg/rp/relaying_party.go b/pkg/client/rp/relaying_party.go similarity index 69% rename from pkg/rp/relaying_party.go rename to pkg/client/rp/relaying_party.go index 3260657..528f554 100644 --- a/pkg/rp/relaying_party.go +++ b/pkg/client/rp/relaying_party.go @@ -4,43 +4,32 @@ import ( "context" "errors" "net/http" - "net/url" - "reflect" "strings" "time" "github.com/google/uuid" - "github.com/gorilla/schema" - - "github.com/caos/oidc/pkg/oidc" - "github.com/caos/oidc/pkg/oidc/grants" - "github.com/caos/oidc/pkg/utils" - "golang.org/x/oauth2" + "gopkg.in/square/go-jose.v2" + + "github.com/caos/oidc/pkg/client" + "github.com/caos/oidc/pkg/oidc" + "github.com/caos/oidc/pkg/utils" ) const ( - idTokenKey = "id_token" - stateParam = "state" - pkceCode = "pkce" - jwtProfileKey = "urn:ietf:params:oauth:grant-type:jwt-bearer" + idTokenKey = "id_token" + stateParam = "state" + pkceCode = "pkce" ) -var ( - encoder = func() utils.Encoder { - e := schema.NewEncoder() - e.RegisterEncoder(oidc.Scopes{}, func(value reflect.Value) string { - return value.Interface().(oidc.Scopes).Encode() - }) - return e - }() -) - -//RelayingParty declares the minimal interface for oidc clients -type RelayingParty interface { +//RelyingParty declares the minimal interface for oidc clients +type RelyingParty interface { //OAuthConfig returns the oauth2 Config OAuthConfig() *oauth2.Config + //Issuer returns the issuer of the oidc config + Issuer() string + //IsPKCE returns if authorization is done using `Authorization Code Flow with Proof Key for Code Exchange (PKCE)` IsPKCE() bool @@ -53,13 +42,16 @@ type RelayingParty interface { //IsOAuth2Only specifies whether relaying party handles only oauth2 or oidc calls IsOAuth2Only() bool - ClientKey() []byte - ClientKeyID() string + //Signer is used if the relaying party uses the JWT Profile + Signer() jose.Signer + + //UserinfoEndpoint returns the userinfo + UserinfoEndpoint() string //IDTokenVerifier returns the verifier interface used for oidc id_token verification IDTokenVerifier() IDTokenVerifier - //ErrorHandler returns the handler used for callback errors + ErrorHandler() func(http.ResponseWriter, *http.Request, string, string, string) } @@ -71,14 +63,12 @@ var ( } ) -type relayingParty struct { +type relyingParty struct { issuer string endpoints Endpoints oauthConfig *oauth2.Config oauth2Only bool pkce bool - clientKey []byte - clientKeyID string httpClient *http.Client cookieHandler *utils.CookieHandler @@ -86,72 +76,79 @@ type relayingParty struct { errorHandler func(http.ResponseWriter, *http.Request, string, string, string) idTokenVerifier IDTokenVerifier verifierOpts []VerifierOption + signer jose.Signer } -func (rp *relayingParty) OAuthConfig() *oauth2.Config { +func (rp *relyingParty) OAuthConfig() *oauth2.Config { return rp.oauthConfig } -func (rp *relayingParty) IsPKCE() bool { +func (rp *relyingParty) Issuer() string { + return rp.issuer +} + +func (rp *relyingParty) IsPKCE() bool { return rp.pkce } -func (rp *relayingParty) CookieHandler() *utils.CookieHandler { +func (rp *relyingParty) CookieHandler() *utils.CookieHandler { return rp.cookieHandler } -func (rp *relayingParty) HttpClient() *http.Client { +func (rp *relyingParty) HttpClient() *http.Client { return rp.httpClient } -func (rp *relayingParty) IsOAuth2Only() bool { +func (rp *relyingParty) IsOAuth2Only() bool { return rp.oauth2Only } -func (rp *relayingParty) ClientKey() []byte { - return rp.clientKey +func (rp *relyingParty) Signer() jose.Signer { + return rp.signer } -func (rp *relayingParty) ClientKeyID() string { - return rp.clientKeyID +func (rp *relyingParty) UserinfoEndpoint() string { + return rp.endpoints.UserinfoURL } -func (rp *relayingParty) IDTokenVerifier() IDTokenVerifier { +func (rp *relyingParty) IDTokenVerifier() IDTokenVerifier { if rp.idTokenVerifier == nil { rp.idTokenVerifier = NewIDTokenVerifier(rp.issuer, rp.oauthConfig.ClientID, NewRemoteKeySet(rp.httpClient, rp.endpoints.JKWsURL), rp.verifierOpts...) } return rp.idTokenVerifier } -func (rp *relayingParty) ErrorHandler() func(http.ResponseWriter, *http.Request, string, string, string) { +func (rp *relyingParty) ErrorHandler() func(http.ResponseWriter, *http.Request, string, string, string) { if rp.errorHandler == nil { rp.errorHandler = DefaultErrorHandler } return rp.errorHandler } -//NewRelayingPartyOAuth creates an (OAuth2) RelayingParty with the given +//NewRelyingPartyOAuth creates an (OAuth2) RelyingParty with the given //OAuth2 Config and possible configOptions //it will use the AuthURL and TokenURL set in config -func NewRelayingPartyOAuth(config *oauth2.Config, options ...Option) (RelayingParty, error) { - rp := &relayingParty{ +func NewRelyingPartyOAuth(config *oauth2.Config, options ...Option) (RelyingParty, error) { + rp := &relyingParty{ oauthConfig: config, httpClient: utils.DefaultHTTPClient, oauth2Only: true, } for _, optFunc := range options { - optFunc(rp) + if err := optFunc(rp); err != nil { + return nil, err + } } return rp, nil } -//NewRelayingPartyOIDC creates an (OIDC) RelayingParty with the given +//NewRelyingPartyOIDC creates an (OIDC) RelyingParty with the given //issuer, clientID, clientSecret, redirectURI, scopes and possible configOptions //it will run discovery on the provided issuer and use the found endpoints -func NewRelayingPartyOIDC(issuer, clientID, clientSecret, redirectURI string, scopes []string, options ...Option) (RelayingParty, error) { - rp := &relayingParty{ +func NewRelyingPartyOIDC(issuer, clientID, clientSecret, redirectURI string, scopes []string, options ...Option) (RelyingParty, error) { + rp := &relyingParty{ issuer: issuer, oauthConfig: &oauth2.Config{ ClientID: clientID, @@ -164,7 +161,9 @@ func NewRelayingPartyOIDC(issuer, clientID, clientSecret, redirectURI string, sc } for _, optFunc := range options { - optFunc(rp) + if err := optFunc(rp); err != nil { + return nil, err + } } endpoints, err := Discover(rp.issuer, rp.httpClient) @@ -178,12 +177,13 @@ func NewRelayingPartyOIDC(issuer, clientID, clientSecret, redirectURI string, sc } //DefaultRPOpts is the type for providing dynamic options to the DefaultRP -type Option func(*relayingParty) +type Option func(*relyingParty) error //WithCookieHandler set a `CookieHandler` for securing the various redirects func WithCookieHandler(cookieHandler *utils.CookieHandler) Option { - return func(rp *relayingParty) { + return func(rp *relyingParty) error { rp.cookieHandler = cookieHandler + return nil } } @@ -191,40 +191,49 @@ func WithCookieHandler(cookieHandler *utils.CookieHandler) Option { //it also sets a `CookieHandler` for securing the various redirects //and exchanging the code challenge func WithPKCE(cookieHandler *utils.CookieHandler) Option { - return func(rp *relayingParty) { + return func(rp *relyingParty) error { rp.pkce = true rp.cookieHandler = cookieHandler + return nil } } //WithHTTPClient provides the ability to set an http client to be used for the relaying party and verifier func WithHTTPClient(client *http.Client) Option { - return func(rp *relayingParty) { + return func(rp *relyingParty) error { rp.httpClient = client + return nil } } func WithErrorHandler(errorHandler ErrorHandler) Option { - return func(rp *relayingParty) { + return func(rp *relyingParty) error { rp.errorHandler = errorHandler + return nil } } func WithVerifierOpts(opts ...VerifierOption) Option { - return func(rp *relayingParty) { + return func(rp *relyingParty) error { rp.verifierOpts = opts + return nil } } func WithClientKey(path string) Option { - return func(rp *relayingParty) { - config, _ := ConfigFromKeyFile(path) - rp.clientKey = []byte(config.Key) - rp.clientKeyID = config.KeyID + return func(rp *relyingParty) error { + config, err := client.ConfigFromKeyFile(path) + if err != nil { + return err + } + rp.signer, err = client.NewSignerFromPrivateKeyByte([]byte(config.Key), config.KeyID) + return err } } //Discover calls the discovery endpoint of the provided issuer and returns the found endpoints +// +//deprecated: use client.Discover func Discover(issuer string, httpClient *http.Client) (Endpoints, error) { wellKnown := strings.TrimSuffix(issuer, "/") + oidc.DiscoveryEndpoint req, err := http.NewRequest("GET", wellKnown, nil) @@ -241,7 +250,7 @@ func Discover(issuer string, httpClient *http.Client) (Endpoints, error) { //AuthURL returns the auth request url //(wrapping the oauth2 `AuthCodeURL`) -func AuthURL(state string, rp RelayingParty, opts ...AuthURLOpt) string { +func AuthURL(state string, rp RelyingParty, opts ...AuthURLOpt) string { authOpts := make([]oauth2.AuthCodeOption, 0) for _, opt := range opts { authOpts = append(authOpts, opt()...) @@ -251,7 +260,7 @@ func AuthURL(state string, rp RelayingParty, opts ...AuthURLOpt) string { //AuthURLHandler extends the `AuthURL` method with a http redirect handler //including handling setting cookie for secure `state` transfer -func AuthURLHandler(stateFn func() string, rp RelayingParty) http.HandlerFunc { +func AuthURLHandler(stateFn func() string, rp RelyingParty) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { opts := make([]AuthURLOpt, 0) state := stateFn() @@ -272,7 +281,7 @@ func AuthURLHandler(stateFn func() string, rp RelayingParty) http.HandlerFunc { } //GenerateAndStoreCodeChallenge generates a PKCE code challenge and stores its verifier into a secure cookie -func GenerateAndStoreCodeChallenge(w http.ResponseWriter, rp RelayingParty) (string, error) { +func GenerateAndStoreCodeChallenge(w http.ResponseWriter, rp RelyingParty) (string, error) { codeVerifier := uuid.New().String() if err := rp.CookieHandler().SetCookie(w, pkceCode, codeVerifier); err != nil { return "", err @@ -282,7 +291,7 @@ func GenerateAndStoreCodeChallenge(w http.ResponseWriter, rp RelayingParty) (str //CodeExchange handles the oauth2 code exchange, extracting and validating the id_token //returning it parsed together with the oauth2 tokens (access, refresh) -func CodeExchange(ctx context.Context, code string, rp RelayingParty, opts ...CodeExchangeOpt) (tokens *oidc.Tokens, err error) { +func CodeExchange(ctx context.Context, code string, rp RelyingParty, opts ...CodeExchangeOpt) (tokens *oidc.Tokens, err error) { ctx = context.WithValue(ctx, oauth2.HTTPClient, rp.HttpClient()) codeOpts := make([]oauth2.AuthCodeOption, 0) for _, opt := range opts { @@ -314,7 +323,7 @@ func CodeExchange(ctx context.Context, code string, rp RelayingParty, opts ...Co //CodeExchangeHandler extends the `CodeExchange` method with a http handler //including cookie handling for secure `state` transfer //and optional PKCE code verifier checking -func CodeExchangeHandler(callback func(http.ResponseWriter, *http.Request, *oidc.Tokens, string), rp RelayingParty) http.HandlerFunc { +func CodeExchangeHandler(callback func(http.ResponseWriter, *http.Request, *oidc.Tokens, string), rp RelyingParty) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { state, err := tryReadStateCookie(w, r, rp) if err != nil { @@ -335,8 +344,8 @@ func CodeExchangeHandler(callback func(http.ResponseWriter, *http.Request, *oidc } codeOpts = append(codeOpts, WithCodeVerifier(codeVerifier)) } - 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 rp.Signer() != nil { + assertion, err := client.SignedJWTProfileAssertion(rp.OAuthConfig().ClientID, []string{rp.Issuer()}, time.Hour, rp.Signer()) if err != nil { http.Error(w, "failed to build assertion: "+err.Error(), http.StatusUnauthorized) return @@ -352,51 +361,21 @@ func CodeExchangeHandler(callback func(http.ResponseWriter, *http.Request, *oidc } } -//ClientCredentials is the `RelayingParty` interface implementation -//handling the oauth2 client credentials grant -func ClientCredentials(ctx context.Context, rp RelayingParty, scopes ...string) (newToken *oauth2.Token, err error) { - return CallTokenEndpointAuthorized(grants.ClientCredentialsGrantBasic(scopes...), rp) -} - -func CallTokenEndpointAuthorized(request interface{}, rp RelayingParty) (newToken *oauth2.Token, err error) { - config := rp.OAuthConfig() - var fn interface{} = utils.AuthorizeBasic(config.ClientID, config.ClientSecret) - if config.Endpoint.AuthStyle == oauth2.AuthStyleInParams { - fn = func(form url.Values) { - form.Set("client_id", config.ClientID) - form.Set("client_secret", config.ClientSecret) - } - } - return callTokenEndpoint(request, fn, rp) -} - -func CallTokenEndpoint(request interface{}, rp RelayingParty) (newToken *oauth2.Token, err error) { - return callTokenEndpoint(request, nil, rp) -} - -func callTokenEndpoint(request interface{}, authFn interface{}, rp RelayingParty) (newToken *oauth2.Token, err error) { - req, err := utils.FormRequest(rp.OAuthConfig().Endpoint.TokenURL, request, encoder, authFn) +//Userinfo will call the OIDC Userinfo Endpoint with the provided token +func Userinfo(token string, rp RelyingParty) (oidc.UserInfo, error) { + req, err := http.NewRequest("GET", rp.UserinfoEndpoint(), nil) if err != nil { return nil, err } - var tokenRes struct { - AccessToken string `json:"access_token"` - TokenType string `json:"token_type"` - ExpiresIn int64 `json:"expires_in"` - RefreshToken string `json:"refresh_token"` - } - if err := utils.HttpRequest(rp.HttpClient(), req, &tokenRes); err != nil { + req.Header.Set("authorization", token) + userinfo := oidc.NewUserInfo() + if err := utils.HttpRequest(rp.HttpClient(), req, &userinfo); err != nil { return nil, err } - return &oauth2.Token{ - AccessToken: tokenRes.AccessToken, - TokenType: tokenRes.TokenType, - RefreshToken: tokenRes.RefreshToken, - Expiry: time.Now().UTC().Add(time.Duration(tokenRes.ExpiresIn) * time.Second), - }, nil + return userinfo, nil } -func trySetStateCookie(w http.ResponseWriter, state string, rp RelayingParty) error { +func trySetStateCookie(w http.ResponseWriter, state string, rp RelyingParty) error { if rp.CookieHandler() != nil { if err := rp.CookieHandler().SetCookie(w, stateParam, state); err != nil { return err @@ -405,7 +384,7 @@ func trySetStateCookie(w http.ResponseWriter, state string, rp RelayingParty) er return nil } -func tryReadStateCookie(w http.ResponseWriter, r *http.Request, rp RelayingParty) (state string, err error) { +func tryReadStateCookie(w http.ResponseWriter, r *http.Request, rp RelyingParty) (state string, err error) { if rp.CookieHandler() == nil { return r.FormValue(stateParam), nil } @@ -417,7 +396,7 @@ func tryReadStateCookie(w http.ResponseWriter, r *http.Request, rp RelayingParty return state, nil } -type OptionFunc func(RelayingParty) +type OptionFunc func(RelyingParty) type Endpoints struct { oauth2.Endpoint @@ -472,9 +451,6 @@ func WithCodeVerifier(codeVerifier string) CodeExchangeOpt { //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), - } + return client.ClientAssertionCodeOptions(clientAssertion) } } diff --git a/pkg/client/rp/tockenexchange.go b/pkg/client/rp/tockenexchange.go new file mode 100644 index 0000000..d5056ae --- /dev/null +++ b/pkg/client/rp/tockenexchange.go @@ -0,0 +1,27 @@ +package rp + +import ( + "context" + + "golang.org/x/oauth2" + + "github.com/caos/oidc/pkg/oidc/grants/tokenexchange" +) + +//TokenExchangeRP extends the `RelyingParty` interface for the *draft* oauth2 `Token Exchange` +type TokenExchangeRP interface { + RelyingParty + + //TokenExchange implement the `Token Exchange Grant` exchanging some token for an other + TokenExchange(context.Context, *tokenexchange.TokenExchangeRequest) (*oauth2.Token, error) +} + +//DelegationTokenExchangeRP extends the `TokenExchangeRP` interface +//for the specific `delegation token` request +type DelegationTokenExchangeRP interface { + TokenExchangeRP + + //DelegationTokenExchange implement the `Token Exchange Grant` + //providing an access token in request for a `delegation` token for a given resource / audience + DelegationTokenExchange(context.Context, string, ...tokenexchange.TokenExchangeOption) (*oauth2.Token, error) +} diff --git a/pkg/rp/verifier.go b/pkg/client/rp/verifier.go similarity index 92% rename from pkg/rp/verifier.go rename to pkg/client/rp/verifier.go index a156f6d..1f45ca8 100644 --- a/pkg/rp/verifier.go +++ b/pkg/client/rp/verifier.go @@ -214,13 +214,3 @@ func (i *idTokenVerifier) ACR() oidc.ACRVerifier { func (i *idTokenVerifier) MaxAge() time.Duration { return i.maxAge } - -//deprecated: Use IDTokenVerifier (or oidc.Verifier) -type Verifier interface { - - //Verify checks the access_token and id_token and returns the `id token claims` - Verify(ctx context.Context, accessToken, idTokenString string) (*oidc.IDTokenClaims, error) - - //VerifyIDToken checks the id_token only and returns its `id token claims` - VerifyIDToken(ctx context.Context, idTokenString string) (*oidc.IDTokenClaims, error) -} diff --git a/pkg/client/rs/resource_server.go b/pkg/client/rs/resource_server.go new file mode 100644 index 0000000..f5dbe69 --- /dev/null +++ b/pkg/client/rs/resource_server.go @@ -0,0 +1,123 @@ +package rs + +import ( + "context" + "errors" + "net/http" + "time" + + "github.com/caos/oidc/pkg/client" + "github.com/caos/oidc/pkg/oidc" + "github.com/caos/oidc/pkg/utils" +) + +type ResourceServer interface { + IntrospectionURL() string + HttpClient() *http.Client + AuthFn() (interface{}, error) +} + +type resourceServer struct { + issuer string + tokenURL string + introspectURL string + httpClient *http.Client + authFn func() (interface{}, error) +} + +func (r *resourceServer) IntrospectionURL() string { + return r.introspectURL +} + +func (r *resourceServer) HttpClient() *http.Client { + return r.httpClient +} + +func (r *resourceServer) AuthFn() (interface{}, error) { + return r.authFn() +} + +func NewResourceServerClientCredentials(issuer, clientID, clientSecret string, option Option) (ResourceServer, error) { + authorizer := func() (interface{}, error) { + return utils.AuthorizeBasic(clientID, clientSecret), nil + } + return newResourceServer(issuer, authorizer, option) +} +func NewResourceServerJWTProfile(issuer, clientID, keyID string, key []byte, options ...Option) (ResourceServer, error) { + signer, err := client.NewSignerFromPrivateKeyByte(key, keyID) + if err != nil { + return nil, err + } + authorizer := func() (interface{}, error) { + assertion, err := client.SignedJWTProfileAssertion(clientID, []string{issuer}, time.Hour, signer) + if err != nil { + return nil, err + } + return client.ClientAssertionFormAuthorization(assertion), nil + } + return newResourceServer(issuer, authorizer, options...) +} + +func newResourceServer(issuer string, authorizer func() (interface{}, error), options ...Option) (*resourceServer, error) { + rs := &resourceServer{ + issuer: issuer, + httpClient: utils.DefaultHTTPClient, + } + for _, optFunc := range options { + optFunc(rs) + } + if rs.introspectURL == "" || rs.tokenURL == "" { + config, err := client.Discover(rs.issuer, rs.httpClient) + if err != nil { + return nil, err + } + rs.tokenURL = config.TokenEndpoint + rs.introspectURL = config.IntrospectionEndpoint + } + if rs.introspectURL == "" || rs.tokenURL == "" { + return nil, errors.New("introspectURL and/or tokenURL is empty: please provide with either `WithStaticEndpoints` or a discovery url") + } + rs.authFn = authorizer + return rs, nil +} + +func NewResourceServerFromKeyFile(issuer, path string, options ...Option) (ResourceServer, error) { + c, err := client.ConfigFromKeyFile(path) + if err != nil { + return nil, err + } + return NewResourceServerJWTProfile(issuer, c.ClientID, c.KeyID, []byte(c.Key), options...) +} + +type Option func(*resourceServer) + +//WithClient provides the ability to set an http client to be used for the resource server +func WithClient(client *http.Client) Option { + return func(server *resourceServer) { + server.httpClient = client + } +} + +//WithStaticEndpoints provides the ability to set static token and introspect URL +func WithStaticEndpoints(tokenURL, introspectURL string) Option { + return func(server *resourceServer) { + server.tokenURL = tokenURL + server.introspectURL = introspectURL + } +} + +func Introspect(ctx context.Context, rp ResourceServer, token string) (oidc.IntrospectionResponse, error) { + authFn, err := rp.AuthFn() + if err != nil { + return nil, err + } + req, err := utils.FormRequest(rp.IntrospectionURL(), &oidc.IntrospectionRequest{Token: token}, client.Encoder, authFn) + 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/oidc/discovery.go b/pkg/oidc/discovery.go index 104078c..ef1d65e 100644 --- a/pkg/oidc/discovery.go +++ b/pkg/oidc/discovery.go @@ -42,6 +42,7 @@ type DiscoveryConfiguration struct { DisplayValuesSupported []Display `json:"display_values_supported,omitempty"` ClaimTypesSupported []string `json:"claim_types_supported,omitempty"` ClaimsSupported []string `json:"claims_supported,omitempty"` + ClaimsParameterSupported bool `json:"claims_parameter_supported,omitempty"` CodeChallengeMethodsSupported []CodeChallengeMethod `json:"code_challenge_methods_supported,omitempty"` ServiceDocumentation string `json:"service_documentation,omitempty"` ClaimsLocalesSupported []language.Tag `json:"claims_locales_supported,omitempty"` diff --git a/pkg/op/storage.go b/pkg/op/storage.go index 967a24c..33ed6ce 100644 --- a/pkg/op/storage.go +++ b/pkg/op/storage.go @@ -34,11 +34,6 @@ type OPStorage interface { 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) - - //deprecated: use GetUserinfoFromScopes instead - GetUserinfoFromScopes(ctx context.Context, userID, clientID string, scopes []string) (oidc.UserInfo, error) - //deprecated: use SetUserinfoFromToken instead - GetUserinfoFromToken(ctx context.Context, tokenID, subject, origin string) (oidc.UserInfo, error) } type Storage interface { diff --git a/pkg/op/token.go b/pkg/op/token.go index 5331d44..334bec9 100644 --- a/pkg/op/token.go +++ b/pkg/op/token.go @@ -114,7 +114,8 @@ func CreateIDToken(ctx context.Context, issuer string, authReq AuthRequest, vali } } if len(scopes) > 0 { - userInfo, err := storage.GetUserinfoFromScopes(ctx, authReq.GetSubject(), authReq.GetClientID(), scopes) + userInfo := oidc.NewUserInfo() + err := storage.SetUserinfoFromScopes(ctx, userInfo, authReq.GetSubject(), authReq.GetClientID(), scopes) if err != nil { return "", err } diff --git a/pkg/op/tokenrequest.go b/pkg/op/tokenrequest.go index e0729bf..b51d2c8 100644 --- a/pkg/op/tokenrequest.go +++ b/pkg/op/tokenrequest.go @@ -123,6 +123,9 @@ func AuthorizeClient(ctx context.Context, tokenReq *oidc.AccessTokenRequest, exc if err != nil { return nil, nil, err } + if client.AuthMethod() == oidc.AuthMethodPrivateKeyJWT { + return nil, nil, errors.New("invalid_grant") + } if client.AuthMethod() == oidc.AuthMethodNone { authReq, err := AuthorizeCodeChallenge(ctx, tokenReq, exchanger) return authReq, client, err diff --git a/pkg/op/userinfo.go b/pkg/op/userinfo.go index d951136..9abf378 100644 --- a/pkg/op/userinfo.go +++ b/pkg/op/userinfo.go @@ -34,7 +34,8 @@ func Userinfo(w http.ResponseWriter, r *http.Request, userinfoProvider UserinfoP http.Error(w, "access token invalid", http.StatusUnauthorized) return } - info, err := userinfoProvider.Storage().GetUserinfoFromToken(r.Context(), tokenID, subject, r.Header.Get("origin")) + info := oidc.NewUserInfo() + err = userinfoProvider.Storage().SetUserinfoFromToken(r.Context(), info, tokenID, subject, r.Header.Get("origin")) if err != nil { w.WriteHeader(http.StatusForbidden) utils.MarshalJSON(w, err) diff --git a/pkg/rp/resource_server.go b/pkg/rp/resource_server.go deleted file mode 100644 index c59097d..0000000 --- a/pkg/rp/resource_server.go +++ /dev/null @@ -1,184 +0,0 @@ -package rp - -import ( - "context" - "errors" - "net/http" - "net/url" - "time" - - "golang.org/x/oauth2" - "golang.org/x/oauth2/clientcredentials" - - "github.com/caos/oidc/pkg/oidc" - "github.com/caos/oidc/pkg/utils" -) - -type ResourceServer interface { - IntrospectionURL() string - HttpClient() *http.Client - AuthFn() interface{} -} - -type resourceServer struct { - issuer string - 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 { - return r.introspectURL -} - -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(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) { - 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) { -// 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, - } - 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())) - rp.authFn = authorizer - return rp, nil -} - -func NewResourceServerFromKeyFile(path string, options ...RSOption) (ResourceServer, error) { - c, err := ConfigFromKeyFile(path) - 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...) -} - -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, rp.AuthFn()) - 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 deleted file mode 100644 index bdca44b..0000000 --- a/pkg/rp/tockenexchange.go +++ /dev/null @@ -1,100 +0,0 @@ -package rp - -import ( - "context" - "crypto/rsa" - "crypto/x509" - "encoding/json" - "encoding/pem" - - "golang.org/x/oauth2" - "gopkg.in/square/go-jose.v2" - - "github.com/caos/oidc/pkg/oidc" - "github.com/caos/oidc/pkg/oidc/grants/tokenexchange" -) - -//TokenExchangeRP extends the `RelayingParty` interface for the *draft* oauth2 `Token Exchange` -type TokenExchangeRP interface { - RelayingParty - - //TokenExchange implement the `Token Exchange Grant` exchanging some token for an other - TokenExchange(context.Context, *tokenexchange.TokenExchangeRequest) (*oauth2.Token, error) -} - -//DelegationTokenExchangeRP extends the `TokenExchangeRP` interface -//for the specific `delegation token` request -type DelegationTokenExchangeRP interface { - TokenExchangeRP - - //DelegationTokenExchange implement the `Token Exchange Grant` - //providing an access token in request for a `delegation` token for a given resource / audience - DelegationTokenExchange(context.Context, string, ...tokenexchange.TokenExchangeOption) (*oauth2.Token, error) -} - -//TokenExchange handles the oauth2 token exchange -func TokenExchange(ctx context.Context, request *tokenexchange.TokenExchangeRequest, rp RelayingParty) (newToken *oauth2.Token, err error) { - return CallTokenEndpoint(request, rp) -} - -//DelegationTokenExchange handles the oauth2 token exchange for a delegation token -func DelegationTokenExchange(ctx context.Context, subjectToken string, rp RelayingParty, reqOpts ...tokenexchange.TokenExchangeOption) (newToken *oauth2.Token, err error) { - return TokenExchange(ctx, DelegationTokenRequest(subjectToken, reqOpts...), rp) -} - -//JWTProfileExchange handles the oauth2 jwt profile exchange -func JWTProfileExchange(ctx context.Context, jwtProfileGrantRequest *oidc.JWTProfileGrantRequest, rp RelayingParty) (*oauth2.Token, error) { - return CallTokenEndpoint(jwtProfileGrantRequest, rp) -} - -//JWTProfileExchange handles the oauth2 jwt profile exchange -func JWTProfileAssertionExchange(ctx context.Context, assertion *oidc.JWTProfileAssertion, scopes oidc.Scopes, rp RelayingParty) (*oauth2.Token, error) { - token, err := GenerateJWTProfileToken(assertion) - if err != nil { - return nil, err - } - return JWTProfileExchange(ctx, oidc.NewJWTProfileGrantRequest(token, scopes...), rp) -} - -func GenerateJWTProfileToken(assertion *oidc.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/utils/key.go b/pkg/utils/key.go new file mode 100644 index 0000000..7965c85 --- /dev/null +++ b/pkg/utils/key.go @@ -0,0 +1,25 @@ +package utils + +import ( + "crypto/rsa" + "crypto/x509" + "encoding/pem" +) + +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 +}