introspect
This commit is contained in:
parent
134999bc33
commit
138da8a208
13 changed files with 305 additions and 98 deletions
|
@ -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())
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
)
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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")
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue