introspect and client assertion
This commit is contained in:
parent
50ab51bb46
commit
960be5af1f
19 changed files with 413 additions and 156 deletions
33
pkg/rp/key.go
Normal file
33
pkg/rp/key.go
Normal 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
|
||||
}
|
|
@ -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
118
pkg/rp/resource_server.go
Normal 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
|
||||
}
|
|
@ -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) {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue