introspect and client assertion
This commit is contained in:
parent
50ab51bb46
commit
960be5af1f
19 changed files with 413 additions and 156 deletions
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
18
pkg/oidc/jwt_profile.go
Normal file
18
pkg/oidc/jwt_profile.go
Normal file
|
@ -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,
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
18
pkg/op/op.go
18
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)))
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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
|
||||
|
|
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