introspect

This commit is contained in:
Livio Amstutz 2021-02-10 16:42:01 +01:00
parent 134999bc33
commit 138da8a208
13 changed files with 305 additions and 98 deletions

View file

@ -27,6 +27,7 @@ var (
func main() {
clientID := os.Getenv("CLIENT_ID")
clientSecret := os.Getenv("CLIENT_SECRET")
keyPath := os.Getenv("KEY_PATH")
issuer := os.Getenv("ISSUER")
port := os.Getenv("PORT")
scopes := strings.Split(os.Getenv("SCOPES"), " ")
@ -35,10 +36,19 @@ func main() {
redirectURI := fmt.Sprintf("http://localhost:%v%v", port, callbackPath)
cookieHandler := utils.NewCookieHandler(key, key, utils.WithUnsecure())
provider, err := rp.NewRelayingPartyOIDC(issuer, clientID, clientSecret, redirectURI, scopes,
rp.WithPKCE(cookieHandler),
rp.WithVerifierOpts(rp.WithIssuedAtOffset(5*time.Second)),
)
options := []rp.Option{
rp.WithCookieHandler(cookieHandler),
rp.WithVerifierOpts(rp.WithIssuedAtOffset(5 * time.Second)),
}
if clientSecret == "" {
options = append(options, rp.WithPKCE(cookieHandler))
}
if keyPath != "" {
options = append(options, rp.WithClientKey(keyPath))
}
provider, err := rp.NewRelayingPartyOIDC(issuer, clientID, clientSecret, redirectURI, scopes, options...)
if err != nil {
logrus.Fatalf("error creating provider %s", err.Error())
}

View file

@ -1,27 +1,56 @@
package oidc
import (
"golang.org/x/text/language"
)
const (
DiscoveryEndpoint = "/.well-known/openid-configuration"
)
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 []CodeChallengeMethod `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"`
RevocationEndpoint string `json:"revocation_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"`
ResponseModesSupported []string `json:"response_modes_supported,omitempty"`
GrantTypesSupported []GrantType `json:"grant_types_supported,omitempty"`
ACRValuesSupported []string `json:"acr_values_supported,omitempty"`
SubjectTypesSupported []string `json:"subject_types_supported,omitempty"`
IDTokenSigningAlgValuesSupported []string `json:"id_token_signing_alg_values_supported,omitempty"`
IDTokenEncryptionAlgValuesSupported []string `json:"id_token_encryption_alg_values_supported,omitempty"`
IDTokenEncryptionEncValuesSupported []string `json:"id_token_encryption_enc_values_supported,omitempty"`
UserinfoSigningAlgValuesSupported []string `json:"userinfo_signing_alg_values_supported,omitempty"`
UserinfoEncryptionAlgValuesSupported []string `json:"userinfo_encryption_alg_values_supported,omitempty"`
UserinfoEncryptionEncValuesSupported []string `json:"userinfo_encryption_enc_values_supported,omitempty"`
RequestObjectSigningAlgValuesSupported []string `json:"request_object_signing_alg_values_supported,omitempty"`
RequestObjectEncryptionAlgValuesSupported []string `json:"request_object_encryption_alg_values_supported,omitempty"`
RequestObjectEncryptionEncValuesSupported []string `json:"request_object_encryption_enc_values_supported,omitempty"`
TokenEndpointAuthMethodsSupported []AuthMethod `json:"token_endpoint_auth_methods_supported,omitempty"`
TokenEndpointAuthSigningAlgValuesSupported []string `json:"token_endpoint_auth_signing_alg_values_supported,omitempty"`
RevocationEndpointAuthMethodsSupported []AuthMethod `json:"revocation_endpoint_auth_methods_supported,omitempty"`
RevocationEndpointAuthSigningAlgValuesSupported []string `json:"revocation_endpoint_auth_signing_alg_values_supported,omitempty"`
IntrospectionEndpointAuthMethodsSupported []AuthMethod `json:"introspection_endpoint_auth_methods_supported,omitempty"`
IntrospectionEndpointAuthSigningAlgValuesSupported []string `json:"introspection_endpoint_auth_signing_alg_values_supported,omitempty"`
DisplayValuesSupported []Display `json:"display_values_supported,omitempty"`
ClaimTypesSupported []string `json:"claim_types_supported,omitempty"`
ClaimsSupported []string `json:"claims_supported,omitempty"`
CodeChallengeMethodsSupported []CodeChallengeMethod `json:"code_challenge_methods_supported,omitempty"`
ServiceDocumentation string `json:"service_documentation,omitempty"`
ClaimsLocalesSupported []language.Tag `json:"claims_locales_supported,omitempty"`
UILocalesSupported []language.Tag `json:"ui_locales_supported,omitempty"`
RequestParameterSupported bool `json:"request_parameter_supported,omitempty"`
RequestURIParameterSupported bool `json:"request_uri_parameter_supported"` //no omitempty because: If omitted, the default value is true
RequireRequestURIRegistration bool `json:"require_request_uri_registration,omitempty"`
OPPolicyURI string `json:"op_policy_uri,omitempty"`
OPTermsOfServiceURI string `json:"op_tos_uri,omitempty"`
}
type AuthMethod string
@ -32,3 +61,7 @@ const (
AuthMethodNone AuthMethod = "none"
AuthMethodPrivateKeyJWT AuthMethod = "private_key_jwt"
)
const (
GrantTypeImplicit GrantType = "implicit"
)

View file

@ -12,10 +12,17 @@ type IntrospectionRequest struct {
Token string `schema:"token"`
}
type ClientAssertionParams struct {
ClientAssertion string `schema:"client_assertion"`
ClientAssertionType string `schema:"client_assertion_type"`
}
type IntrospectionResponse interface {
UserInfoSetter
SetActive(bool)
IsActive() bool
SetScopes(scopes Scope)
SetClientID(id string)
}
func NewIntrospectionResponse() IntrospectionResponse {
@ -23,19 +30,30 @@ func NewIntrospectionResponse() IntrospectionResponse {
}
type introspectionResponse struct {
Active bool `json:"active"`
Subject string `json:"sub,omitempty"`
Active bool `json:"active"`
Scope Scope `json:"scope,omitempty"`
ClientID string `json:"client_id,omitempty"`
Subject string `json:"sub,omitempty"`
userInfoProfile
userInfoEmail
userInfoPhone
Address UserInfoAddress `json:"address,omitempty"`
claims map[string]interface{}
Address UserInfoAddress `json:"address,omitempty"`
claims map[string]interface{}
}
func (u *introspectionResponse) IsActive() bool {
return u.Active
}
func (u *introspectionResponse) SetScopes(scope Scope) {
u.Scope = scope
}
func (u *introspectionResponse) SetClientID(id string) {
u.ClientID = id
}
func (u *introspectionResponse) GetSubject() string {
return u.Subject
}

View file

@ -427,7 +427,7 @@ func NewJWTProfileAssertionStringFromFileData(data []byte, audience []string) (s
if err != nil {
return "", err
}
return generateJWTProfileToken(NewJWTProfileAssertion(keyData.UserID, keyData.KeyID, audience, []byte(keyData.Key)))
return GenerateJWTProfileToken(NewJWTProfileAssertion(keyData.UserID, keyData.KeyID, audience, []byte(keyData.Key)))
}
func NewJWTProfileAssertionFromFileData(data []byte, audience []string) (*JWTProfileAssertion, error) {
@ -473,7 +473,7 @@ func AppendClientIDToAudience(clientID string, audience []string) []string {
return append(audience, clientID)
}
func generateJWTProfileToken(assertion *JWTProfileAssertion) (string, error) {
func GenerateJWTProfileToken(assertion *JWTProfileAssertion) (string, error) {
privateKey, err := bytesToPrivateKey(assertion.PrivateKey)
if err != nil {
return "", err

View file

@ -59,6 +59,7 @@ type Prompt string
type ResponseType string
type Scopes []string
type Scope []string //TODO: hurst?
func (s Scopes) Encode() string {
return strings.Join(s, " ")
@ -73,6 +74,19 @@ func (s *Scopes) MarshalText() ([]byte, error) {
return []byte(s.Encode()), nil
}
func (s *Scope) MarshalJSON() ([]byte, error) {
return json.Marshal(Scopes(*s).Encode())
}
func (s *Scope) UnmarshalJSON(data []byte) error {
var str string
if err := json.Unmarshal(data, &str); err != nil {
return err
}
*s = Scope(strings.Split(str, " "))
return nil
}
type Time time.Time
func (t *Time) UnmarshalJSON(data []byte) error {

View file

@ -6,8 +6,6 @@ import (
"time"
"golang.org/x/text/language"
"github.com/caos/oidc/pkg/utils"
)
type UserInfo interface {
@ -351,11 +349,17 @@ func (i *userinfo) MarshalJSON() ([]byte, error) {
return b, nil
}
claims, err := json.Marshal(i.claims)
err = json.Unmarshal(b, &i.claims)
if err != nil {
return nil, fmt.Errorf("jws: invalid map of custom claims %v", i.claims)
}
return utils.ConcatenateJSON(b, claims)
return json.Marshal(i.claims)
//claims, err := json.Marshal(i.claims)
//if err != nil {
// return nil, fmt.Errorf("jws: invalid map of custom claims %v", i.claims)
//}
//return utils.ConcatenateJSON(b, claims)
}
func (i *userinfo) UnmarshalJSON(data []byte) error {
@ -372,6 +376,10 @@ func (i *userinfo) 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
}

View file

@ -3,6 +3,8 @@ package op
import (
"net/http"
"golang.org/x/text/language"
"github.com/caos/oidc/pkg/oidc"
"github.com/caos/oidc/pkg/utils"
)
@ -24,17 +26,22 @@ func CreateDiscoveryConfig(c Configuration, s Signer) *oidc.DiscoveryConfigurati
TokenEndpoint: c.TokenEndpoint().Absolute(c.Issuer()),
IntrospectionEndpoint: c.IntrospectionEndpoint().Absolute(c.Issuer()),
UserinfoEndpoint: c.UserinfoEndpoint().Absolute(c.Issuer()),
EndSessionEndpoint: c.EndSessionEndpoint().Absolute(c.Issuer()),
//RevocationEndpoint: c.RevocationEndpoint().Absolute(c.Issuer()),
EndSessionEndpoint: c.EndSessionEndpoint().Absolute(c.Issuer()),
// CheckSessionIframe: c.TokenEndpoint().Absolute(c.Issuer())(c.CheckSessionIframe),
JwksURI: c.KeysEndpoint().Absolute(c.Issuer()),
ScopesSupported: Scopes(c),
ResponseTypesSupported: ResponseTypes(c),
GrantTypesSupported: GrantTypes(c),
ClaimsSupported: SupportedClaims(c),
IDTokenSigningAlgValuesSupported: SigAlgorithms(s),
SubjectTypesSupported: SubjectTypes(c),
TokenEndpointAuthMethodsSupported: AuthMethods(c),
CodeChallengeMethodsSupported: CodeChallengeMethods(c),
JwksURI: c.KeysEndpoint().Absolute(c.Issuer()),
ScopesSupported: Scopes(c),
ResponseTypesSupported: ResponseTypes(c),
//ResponseModesSupported:
GrantTypesSupported: GrantTypes(c),
//ACRValuesSupported: ACRValues(c),
SubjectTypesSupported: SubjectTypes(c),
IDTokenSigningAlgValuesSupported: SigAlgorithms(s),
TokenEndpointAuthMethodsSupported: AuthMethodsTokenEndpoint(c),
IntrospectionEndpointAuthMethodsSupported: AuthMethodsIntrospectionEndpoint(c),
ClaimsSupported: SupportedClaims(c),
CodeChallengeMethodsSupported: CodeChallengeMethods(c),
UILocalesSupported: UILocales(c),
}
}
@ -58,15 +65,16 @@ func ResponseTypes(c Configuration) []string {
} //TODO: ok for now, check later if dynamic needed
}
func GrantTypes(c Configuration) []string {
grantTypes := []string{
string(oidc.GrantTypeCode),
func GrantTypes(c Configuration) []oidc.GrantType {
grantTypes := []oidc.GrantType{
oidc.GrantTypeCode,
oidc.GrantTypeImplicit,
}
if c.GrantTypeTokenExchangeSupported() {
grantTypes = append(grantTypes, string(oidc.GrantTypeTokenExchange))
grantTypes = append(grantTypes, oidc.GrantTypeTokenExchange)
}
if c.GrantTypeJWTAuthorizationSupported() {
grantTypes = append(grantTypes, string(oidc.GrantTypeBearer))
grantTypes = append(grantTypes, oidc.GrantTypeBearer)
}
return grantTypes
}
@ -108,7 +116,7 @@ func SubjectTypes(c Configuration) []string {
return []string{"public"} //TODO: config
}
func AuthMethods(c Configuration) []oidc.AuthMethod {
func AuthMethodsTokenEndpoint(c Configuration) []oidc.AuthMethod {
authMethods := []oidc.AuthMethod{
oidc.AuthMethodNone,
oidc.AuthMethodBasic,
@ -122,6 +130,16 @@ func AuthMethods(c Configuration) []oidc.AuthMethod {
return authMethods
}
func AuthMethodsIntrospectionEndpoint(c Configuration) []oidc.AuthMethod {
authMethods := []oidc.AuthMethod{
oidc.AuthMethodBasic,
}
if c.AuthMethodPrivateKeyJWTSupported() {
authMethods = append(authMethods, oidc.AuthMethodPrivateKeyJWT)
}
return authMethods
}
func CodeChallengeMethods(c Configuration) []oidc.CodeChallengeMethod {
codeMethods := make([]oidc.CodeChallengeMethod, 0, 1)
if c.CodeMethodS256Supported() {
@ -129,3 +147,10 @@ func CodeChallengeMethods(c Configuration) []oidc.CodeChallengeMethod {
}
return codeMethods
}
func UILocales(c Configuration) []language.Tag {
return []language.Tag{
language.English,
language.German,
}
}

View file

@ -30,7 +30,7 @@ type OPStorage interface {
AuthorizeClientIDSecret(ctx context.Context, clientID, clientSecret string) error
SetUserinfoFromScopes(ctx context.Context, userinfo oidc.UserInfoSetter, userID, clientID string, scopes []string) error
SetUserinfoFromToken(ctx context.Context, userinfo oidc.UserInfoSetter, tokenID, subject, origin string) error
SetIntrospectionFromToken(ctx context.Context, userinfo oidc.IntrospectionResponse, tokenID, subject, callerTokenID, callerSubject string) error
SetIntrospectionFromToken(ctx context.Context, userinfo oidc.IntrospectionResponse, tokenID, subject, clientID string) error
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)

View file

@ -3,7 +3,6 @@ package op
import (
"errors"
"net/http"
"strings"
"github.com/caos/oidc/pkg/oidc"
"github.com/caos/oidc/pkg/utils"
@ -16,6 +15,11 @@ type Introspector interface {
AccessTokenVerifier() AccessTokenVerifier
}
type IntrospectorJWTProfile interface {
Introspector
JWTProfileVerifier() JWTProfileVerifier
}
func introspectionHandler(introspector Introspector) func(http.ResponseWriter, *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
Introspect(w, r, introspector)
@ -23,24 +27,18 @@ func introspectionHandler(introspector Introspector) func(http.ResponseWriter, *
}
func Introspect(w http.ResponseWriter, r *http.Request, introspector Introspector) {
callerToken := r.Header.Get("authorization")
response := oidc.NewIntrospectionResponse()
callerToken, callerSubject, ok := getTokenIDAndSubject(r.Context(), introspector, strings.TrimPrefix(callerToken, oidc.PrefixBearer))
if !ok {
utils.MarshalJSON(w, response)
return
}
introspectionToken, err := ParseTokenInrospectionRequest(r, introspector.Decoder())
token, clientID, err := ParseTokenIntrospectionRequest(r, introspector)
if err != nil {
utils.MarshalJSON(w, response)
http.Error(w, err.Error(), http.StatusUnauthorized)
return
}
tokenID, subject, ok := getTokenIDAndSubject(r.Context(), introspector, introspectionToken)
tokenID, subject, ok := getTokenIDAndSubject(r.Context(), introspector, token)
if !ok {
utils.MarshalJSON(w, response)
return
}
err = introspector.Storage().SetIntrospectionFromToken(r.Context(), response, tokenID, subject, callerToken, callerSubject)
err = introspector.Storage().SetIntrospectionFromToken(r.Context(), response, tokenID, subject, clientID)
if err != nil {
utils.MarshalJSON(w, response)
return
@ -49,15 +47,31 @@ func Introspect(w http.ResponseWriter, r *http.Request, introspector Introspecto
utils.MarshalJSON(w, response)
}
func ParseTokenInrospectionRequest(r *http.Request, decoder utils.Decoder) (string, error) {
err := r.ParseForm()
func ParseTokenIntrospectionRequest(r *http.Request, introspector Introspector) (token, clientID string, err error) {
err = r.ParseForm()
if err != nil {
return "", errors.New("unable to parse request")
return "", "", errors.New("unable to parse request")
}
req := new(oidc.IntrospectionRequest)
err = decoder.Decode(req, r.Form)
req := new(struct {
oidc.IntrospectionRequest
oidc.ClientAssertionParams
})
err = introspector.Decoder().Decode(req, r.Form)
if err != nil {
return "", errors.New("unable to parse request")
return "", "", errors.New("unable to parse request")
}
return req.Token, nil
if introspectorJWTProfile, ok := introspector.(IntrospectorJWTProfile); ok && req.ClientAssertion != "" {
profile, err := VerifyJWTAssertion(r.Context(), req.ClientAssertion, introspectorJWTProfile.JWTProfileVerifier())
if err == nil {
return req.Token, profile.Issuer, nil
}
}
clientID, clientSecret, ok := r.BasicAuth()
if ok {
if err := introspector.Storage().AuthorizeClientIDSecret(r.Context(), clientID, clientSecret); err != nil {
return "", "", err
}
return req.Token, clientID, nil
}
return "", "", errors.New("invalid authorization")
}

View file

@ -24,7 +24,7 @@ func userinfoHandler(userinfoProvider UserinfoProvider) func(http.ResponseWriter
}
func Userinfo(w http.ResponseWriter, r *http.Request, userinfoProvider UserinfoProvider) {
accessToken, err := getAccessToken(r, userinfoProvider.Decoder())
accessToken, err := ParseUserinfoRequest(r, userinfoProvider.Decoder())
if err != nil {
http.Error(w, "access token missing", http.StatusUnauthorized)
return
@ -43,16 +43,12 @@ func Userinfo(w http.ResponseWriter, r *http.Request, userinfoProvider UserinfoP
utils.MarshalJSON(w, info)
}
func getAccessToken(r *http.Request, decoder utils.Decoder) (string, error) {
authHeader := r.Header.Get("authorization")
if authHeader != "" {
parts := strings.Split(authHeader, "Bearer ")
if len(parts) != 2 {
return "", errors.New("invalid auth header")
}
return parts[1], nil
func ParseUserinfoRequest(r *http.Request, decoder utils.Decoder) (string, error) {
accessToken, err := getAccessToken(r)
if err == nil {
return accessToken, nil
}
err := r.ParseForm()
err = r.ParseForm()
if err != nil {
return "", errors.New("unable to parse request")
}
@ -64,6 +60,18 @@ func getAccessToken(r *http.Request, decoder utils.Decoder) (string, error) {
return req.AccessToken, nil
}
func getAccessToken(r *http.Request) (string, error) {
authHeader := r.Header.Get("authorization")
if authHeader == "" {
return "", errors.New("no auth header")
}
parts := strings.Split(authHeader, "Bearer ")
if len(parts) != 2 {
return "", errors.New("invalid auth header")
}
return parts[1], nil
}
func getTokenIDAndSubject(ctx context.Context, userinfoProvider UserinfoProvider, accessToken string) (string, string, bool) {
tokenIDSubject, err := userinfoProvider.Crypto().Decrypt(accessToken)
if err == nil {

View file

@ -216,6 +216,14 @@ func WithVerifierOpts(opts ...VerifierOption) Option {
}
}
func WithClientKey(path string) Option {
return func(rp *relayingParty) {
config, _ := ConfigFromKeyFile(path)
rp.clientKey = []byte(config.Key)
rp.clientKeyID = config.KeyID
}
}
//Discover calls the discovery endpoint of the provided issuer and returns the found endpoints
func Discover(issuer string, httpClient *http.Client) (Endpoints, error) {
wellKnown := strings.TrimSuffix(issuer, "/") + oidc.DiscoveryEndpoint
@ -327,14 +335,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))
//}
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 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)

View file

@ -4,10 +4,11 @@ import (
"context"
"errors"
"net/http"
"net/url"
"time"
"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"
@ -16,6 +17,7 @@ import (
type ResourceServer interface {
IntrospectionURL() string
HttpClient() *http.Client
AuthFn() interface{}
}
type resourceServer struct {
@ -23,6 +25,32 @@ type resourceServer struct {
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 {
@ -33,8 +61,12 @@ 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(ctx context.Context) *http.Client {
authorizer := func(tokenURL string) func(context.Context) *http.Client {
return (&clientcredentials.Config{
ClientID: clientID,
ClientSecret: clientSecret,
@ -44,20 +76,52 @@ func NewResourceServerClientCredentials(issuer, clientID, clientSecret string, o
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
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) {
//
//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,
@ -76,7 +140,8 @@ func newResourceServer(issuer string, authorizer func(tokenURL string) func(ctx
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.httpClient = authorizer(rp.tokenURL)(context.WithValue(context.Background(), oauth2.HTTPClient, rp.HttpClient()))
rp.authFn = authorizer
return rp, nil
}
@ -85,6 +150,7 @@ func NewResourceServerFromKeyFile(path string, options ...RSOption) (ResourceSer
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...)
}
@ -106,7 +172,7 @@ func WithStaticEndpoints(tokenURL, introspectURL string) RSOption {
}
func Introspect(ctx context.Context, rp ResourceServer, token string) (oidc.IntrospectionResponse, error) {
req, err := utils.FormRequest(rp.IntrospectionURL(), &oidc.IntrospectionRequest{Token: token}, encoder, nil)
req, err := utils.FormRequest(rp.IntrospectionURL(), &oidc.IntrospectionRequest{Token: token}, encoder, rp.AuthFn())
if err != nil {
return nil, err
}

View file

@ -42,6 +42,9 @@ func FormRequest(endpoint string, request interface{}, encoder Encoder, authFn i
if fn, ok := authFn.(FormAuthorization); ok {
fn(form)
}
if fn, ok := authFn.(func(url.Values)); ok {
fn(form)
}
body := strings.NewReader(form.Encode())
req, err := http.NewRequest("POST", endpoint, body)
if err != nil {