introspect and client assertion

This commit is contained in:
Livio Amstutz 2021-02-01 17:17:40 +01:00
parent 50ab51bb46
commit 960be5af1f
19 changed files with 413 additions and 156 deletions

33
pkg/rp/key.go Normal file
View file

@ -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
}

View file

@ -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),
}
}
}

118
pkg/rp/resource_server.go Normal file
View file

@ -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
}

View file

@ -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) {