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() {
|
func main() {
|
||||||
clientID := os.Getenv("CLIENT_ID")
|
clientID := os.Getenv("CLIENT_ID")
|
||||||
clientSecret := os.Getenv("CLIENT_SECRET")
|
clientSecret := os.Getenv("CLIENT_SECRET")
|
||||||
|
keyPath := os.Getenv("KEY_PATH")
|
||||||
issuer := os.Getenv("ISSUER")
|
issuer := os.Getenv("ISSUER")
|
||||||
port := os.Getenv("PORT")
|
port := os.Getenv("PORT")
|
||||||
scopes := strings.Split(os.Getenv("SCOPES"), " ")
|
scopes := strings.Split(os.Getenv("SCOPES"), " ")
|
||||||
|
@ -35,10 +36,19 @@ func main() {
|
||||||
|
|
||||||
redirectURI := fmt.Sprintf("http://localhost:%v%v", port, callbackPath)
|
redirectURI := fmt.Sprintf("http://localhost:%v%v", port, callbackPath)
|
||||||
cookieHandler := utils.NewCookieHandler(key, key, utils.WithUnsecure())
|
cookieHandler := utils.NewCookieHandler(key, key, utils.WithUnsecure())
|
||||||
provider, err := rp.NewRelayingPartyOIDC(issuer, clientID, clientSecret, redirectURI, scopes,
|
|
||||||
rp.WithPKCE(cookieHandler),
|
options := []rp.Option{
|
||||||
|
rp.WithCookieHandler(cookieHandler),
|
||||||
rp.WithVerifierOpts(rp.WithIssuedAtOffset(5 * time.Second)),
|
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 {
|
if err != nil {
|
||||||
logrus.Fatalf("error creating provider %s", err.Error())
|
logrus.Fatalf("error creating provider %s", err.Error())
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,9 @@
|
||||||
package oidc
|
package oidc
|
||||||
|
|
||||||
|
import (
|
||||||
|
"golang.org/x/text/language"
|
||||||
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
DiscoveryEndpoint = "/.well-known/openid-configuration"
|
DiscoveryEndpoint = "/.well-known/openid-configuration"
|
||||||
)
|
)
|
||||||
|
@ -10,18 +14,43 @@ type DiscoveryConfiguration struct {
|
||||||
TokenEndpoint string `json:"token_endpoint,omitempty"`
|
TokenEndpoint string `json:"token_endpoint,omitempty"`
|
||||||
IntrospectionEndpoint string `json:"introspection_endpoint,omitempty"`
|
IntrospectionEndpoint string `json:"introspection_endpoint,omitempty"`
|
||||||
UserinfoEndpoint string `json:"userinfo_endpoint,omitempty"`
|
UserinfoEndpoint string `json:"userinfo_endpoint,omitempty"`
|
||||||
|
RevocationEndpoint string `json:"revocation_endpoint,omitempty"`
|
||||||
EndSessionEndpoint string `json:"end_session_endpoint,omitempty"`
|
EndSessionEndpoint string `json:"end_session_endpoint,omitempty"`
|
||||||
CheckSessionIframe string `json:"check_session_iframe,omitempty"`
|
CheckSessionIframe string `json:"check_session_iframe,omitempty"`
|
||||||
JwksURI string `json:"jwks_uri,omitempty"`
|
JwksURI string `json:"jwks_uri,omitempty"`
|
||||||
ScopesSupported []string `json:"scopes_supported,omitempty"`
|
ScopesSupported []string `json:"scopes_supported,omitempty"`
|
||||||
ResponseTypesSupported []string `json:"response_types_supported,omitempty"`
|
ResponseTypesSupported []string `json:"response_types_supported"`
|
||||||
ResponseModesSupported []string `json:"response_modes_supported,omitempty"`
|
ResponseModesSupported []string `json:"response_modes_supported,omitempty"`
|
||||||
GrantTypesSupported []string `json:"grant_types_supported,omitempty"`
|
GrantTypesSupported []GrantType `json:"grant_types_supported,omitempty"`
|
||||||
|
ACRValuesSupported []string `json:"acr_values_supported,omitempty"`
|
||||||
SubjectTypesSupported []string `json:"subject_types_supported,omitempty"`
|
SubjectTypesSupported []string `json:"subject_types_supported,omitempty"`
|
||||||
IDTokenSigningAlgValuesSupported []string `json:"id_token_signing_alg_values_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"`
|
TokenEndpointAuthMethodsSupported []AuthMethod `json:"token_endpoint_auth_methods_supported,omitempty"`
|
||||||
CodeChallengeMethodsSupported []CodeChallengeMethod `json:"code_challenge_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"`
|
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
|
type AuthMethod string
|
||||||
|
@ -32,3 +61,7 @@ const (
|
||||||
AuthMethodNone AuthMethod = "none"
|
AuthMethodNone AuthMethod = "none"
|
||||||
AuthMethodPrivateKeyJWT AuthMethod = "private_key_jwt"
|
AuthMethodPrivateKeyJWT AuthMethod = "private_key_jwt"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
GrantTypeImplicit GrantType = "implicit"
|
||||||
|
)
|
||||||
|
|
|
@ -12,10 +12,17 @@ type IntrospectionRequest struct {
|
||||||
Token string `schema:"token"`
|
Token string `schema:"token"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ClientAssertionParams struct {
|
||||||
|
ClientAssertion string `schema:"client_assertion"`
|
||||||
|
ClientAssertionType string `schema:"client_assertion_type"`
|
||||||
|
}
|
||||||
|
|
||||||
type IntrospectionResponse interface {
|
type IntrospectionResponse interface {
|
||||||
UserInfoSetter
|
UserInfoSetter
|
||||||
SetActive(bool)
|
SetActive(bool)
|
||||||
IsActive() bool
|
IsActive() bool
|
||||||
|
SetScopes(scopes Scope)
|
||||||
|
SetClientID(id string)
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewIntrospectionResponse() IntrospectionResponse {
|
func NewIntrospectionResponse() IntrospectionResponse {
|
||||||
|
@ -24,18 +31,29 @@ func NewIntrospectionResponse() IntrospectionResponse {
|
||||||
|
|
||||||
type introspectionResponse struct {
|
type introspectionResponse struct {
|
||||||
Active bool `json:"active"`
|
Active bool `json:"active"`
|
||||||
|
Scope Scope `json:"scope,omitempty"`
|
||||||
|
ClientID string `json:"client_id,omitempty"`
|
||||||
Subject string `json:"sub,omitempty"`
|
Subject string `json:"sub,omitempty"`
|
||||||
userInfoProfile
|
userInfoProfile
|
||||||
userInfoEmail
|
userInfoEmail
|
||||||
userInfoPhone
|
userInfoPhone
|
||||||
Address UserInfoAddress `json:"address,omitempty"`
|
|
||||||
|
|
||||||
|
Address UserInfoAddress `json:"address,omitempty"`
|
||||||
claims map[string]interface{}
|
claims map[string]interface{}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *introspectionResponse) IsActive() bool {
|
func (u *introspectionResponse) IsActive() bool {
|
||||||
return u.Active
|
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 {
|
func (u *introspectionResponse) GetSubject() string {
|
||||||
return u.Subject
|
return u.Subject
|
||||||
}
|
}
|
||||||
|
|
|
@ -427,7 +427,7 @@ func NewJWTProfileAssertionStringFromFileData(data []byte, audience []string) (s
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
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) {
|
func NewJWTProfileAssertionFromFileData(data []byte, audience []string) (*JWTProfileAssertion, error) {
|
||||||
|
@ -473,7 +473,7 @@ func AppendClientIDToAudience(clientID string, audience []string) []string {
|
||||||
return append(audience, clientID)
|
return append(audience, clientID)
|
||||||
}
|
}
|
||||||
|
|
||||||
func generateJWTProfileToken(assertion *JWTProfileAssertion) (string, error) {
|
func GenerateJWTProfileToken(assertion *JWTProfileAssertion) (string, error) {
|
||||||
privateKey, err := bytesToPrivateKey(assertion.PrivateKey)
|
privateKey, err := bytesToPrivateKey(assertion.PrivateKey)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
|
|
|
@ -59,6 +59,7 @@ type Prompt string
|
||||||
type ResponseType string
|
type ResponseType string
|
||||||
|
|
||||||
type Scopes []string
|
type Scopes []string
|
||||||
|
type Scope []string //TODO: hurst?
|
||||||
|
|
||||||
func (s Scopes) Encode() string {
|
func (s Scopes) Encode() string {
|
||||||
return strings.Join(s, " ")
|
return strings.Join(s, " ")
|
||||||
|
@ -73,6 +74,19 @@ func (s *Scopes) MarshalText() ([]byte, error) {
|
||||||
return []byte(s.Encode()), nil
|
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
|
type Time time.Time
|
||||||
|
|
||||||
func (t *Time) UnmarshalJSON(data []byte) error {
|
func (t *Time) UnmarshalJSON(data []byte) error {
|
||||||
|
|
|
@ -6,8 +6,6 @@ import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"golang.org/x/text/language"
|
"golang.org/x/text/language"
|
||||||
|
|
||||||
"github.com/caos/oidc/pkg/utils"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type UserInfo interface {
|
type UserInfo interface {
|
||||||
|
@ -351,11 +349,17 @@ func (i *userinfo) MarshalJSON() ([]byte, error) {
|
||||||
return b, nil
|
return b, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
claims, err := json.Marshal(i.claims)
|
err = json.Unmarshal(b, &i.claims)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("jws: invalid map of custom claims %v", i.claims)
|
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 {
|
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())
|
i.UpdatedAt = Time(time.Unix(a.UpdatedAt, 0).UTC())
|
||||||
|
|
||||||
|
if err := json.Unmarshal(data, &i.claims); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -3,6 +3,8 @@ package op
|
||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
|
"golang.org/x/text/language"
|
||||||
|
|
||||||
"github.com/caos/oidc/pkg/oidc"
|
"github.com/caos/oidc/pkg/oidc"
|
||||||
"github.com/caos/oidc/pkg/utils"
|
"github.com/caos/oidc/pkg/utils"
|
||||||
)
|
)
|
||||||
|
@ -24,17 +26,22 @@ func CreateDiscoveryConfig(c Configuration, s Signer) *oidc.DiscoveryConfigurati
|
||||||
TokenEndpoint: c.TokenEndpoint().Absolute(c.Issuer()),
|
TokenEndpoint: c.TokenEndpoint().Absolute(c.Issuer()),
|
||||||
IntrospectionEndpoint: c.IntrospectionEndpoint().Absolute(c.Issuer()),
|
IntrospectionEndpoint: c.IntrospectionEndpoint().Absolute(c.Issuer()),
|
||||||
UserinfoEndpoint: c.UserinfoEndpoint().Absolute(c.Issuer()),
|
UserinfoEndpoint: c.UserinfoEndpoint().Absolute(c.Issuer()),
|
||||||
|
//RevocationEndpoint: c.RevocationEndpoint().Absolute(c.Issuer()),
|
||||||
EndSessionEndpoint: c.EndSessionEndpoint().Absolute(c.Issuer()),
|
EndSessionEndpoint: c.EndSessionEndpoint().Absolute(c.Issuer()),
|
||||||
// CheckSessionIframe: c.TokenEndpoint().Absolute(c.Issuer())(c.CheckSessionIframe),
|
// CheckSessionIframe: c.TokenEndpoint().Absolute(c.Issuer())(c.CheckSessionIframe),
|
||||||
JwksURI: c.KeysEndpoint().Absolute(c.Issuer()),
|
JwksURI: c.KeysEndpoint().Absolute(c.Issuer()),
|
||||||
ScopesSupported: Scopes(c),
|
ScopesSupported: Scopes(c),
|
||||||
ResponseTypesSupported: ResponseTypes(c),
|
ResponseTypesSupported: ResponseTypes(c),
|
||||||
|
//ResponseModesSupported:
|
||||||
GrantTypesSupported: GrantTypes(c),
|
GrantTypesSupported: GrantTypes(c),
|
||||||
ClaimsSupported: SupportedClaims(c),
|
//ACRValuesSupported: ACRValues(c),
|
||||||
IDTokenSigningAlgValuesSupported: SigAlgorithms(s),
|
|
||||||
SubjectTypesSupported: SubjectTypes(c),
|
SubjectTypesSupported: SubjectTypes(c),
|
||||||
TokenEndpointAuthMethodsSupported: AuthMethods(c),
|
IDTokenSigningAlgValuesSupported: SigAlgorithms(s),
|
||||||
|
TokenEndpointAuthMethodsSupported: AuthMethodsTokenEndpoint(c),
|
||||||
|
IntrospectionEndpointAuthMethodsSupported: AuthMethodsIntrospectionEndpoint(c),
|
||||||
|
ClaimsSupported: SupportedClaims(c),
|
||||||
CodeChallengeMethodsSupported: CodeChallengeMethods(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
|
} //TODO: ok for now, check later if dynamic needed
|
||||||
}
|
}
|
||||||
|
|
||||||
func GrantTypes(c Configuration) []string {
|
func GrantTypes(c Configuration) []oidc.GrantType {
|
||||||
grantTypes := []string{
|
grantTypes := []oidc.GrantType{
|
||||||
string(oidc.GrantTypeCode),
|
oidc.GrantTypeCode,
|
||||||
|
oidc.GrantTypeImplicit,
|
||||||
}
|
}
|
||||||
if c.GrantTypeTokenExchangeSupported() {
|
if c.GrantTypeTokenExchangeSupported() {
|
||||||
grantTypes = append(grantTypes, string(oidc.GrantTypeTokenExchange))
|
grantTypes = append(grantTypes, oidc.GrantTypeTokenExchange)
|
||||||
}
|
}
|
||||||
if c.GrantTypeJWTAuthorizationSupported() {
|
if c.GrantTypeJWTAuthorizationSupported() {
|
||||||
grantTypes = append(grantTypes, string(oidc.GrantTypeBearer))
|
grantTypes = append(grantTypes, oidc.GrantTypeBearer)
|
||||||
}
|
}
|
||||||
return grantTypes
|
return grantTypes
|
||||||
}
|
}
|
||||||
|
@ -108,7 +116,7 @@ func SubjectTypes(c Configuration) []string {
|
||||||
return []string{"public"} //TODO: config
|
return []string{"public"} //TODO: config
|
||||||
}
|
}
|
||||||
|
|
||||||
func AuthMethods(c Configuration) []oidc.AuthMethod {
|
func AuthMethodsTokenEndpoint(c Configuration) []oidc.AuthMethod {
|
||||||
authMethods := []oidc.AuthMethod{
|
authMethods := []oidc.AuthMethod{
|
||||||
oidc.AuthMethodNone,
|
oidc.AuthMethodNone,
|
||||||
oidc.AuthMethodBasic,
|
oidc.AuthMethodBasic,
|
||||||
|
@ -122,6 +130,16 @@ func AuthMethods(c Configuration) []oidc.AuthMethod {
|
||||||
return authMethods
|
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 {
|
func CodeChallengeMethods(c Configuration) []oidc.CodeChallengeMethod {
|
||||||
codeMethods := make([]oidc.CodeChallengeMethod, 0, 1)
|
codeMethods := make([]oidc.CodeChallengeMethod, 0, 1)
|
||||||
if c.CodeMethodS256Supported() {
|
if c.CodeMethodS256Supported() {
|
||||||
|
@ -129,3 +147,10 @@ func CodeChallengeMethods(c Configuration) []oidc.CodeChallengeMethod {
|
||||||
}
|
}
|
||||||
return codeMethods
|
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
|
AuthorizeClientIDSecret(ctx context.Context, clientID, clientSecret string) error
|
||||||
SetUserinfoFromScopes(ctx context.Context, userinfo oidc.UserInfoSetter, userID, clientID string, scopes []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
|
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)
|
GetPrivateClaimsFromScopes(ctx context.Context, userID, clientID string, scopes []string) (map[string]interface{}, error)
|
||||||
GetKeyByIDAndUserID(ctx context.Context, keyID, userID string) (*jose.JSONWebKey, error)
|
GetKeyByIDAndUserID(ctx context.Context, keyID, userID string) (*jose.JSONWebKey, error)
|
||||||
ValidateJWTProfileScopes(ctx context.Context, userID string, scope oidc.Scopes) (oidc.Scopes, error)
|
ValidateJWTProfileScopes(ctx context.Context, userID string, scope oidc.Scopes) (oidc.Scopes, error)
|
||||||
|
|
|
@ -3,7 +3,6 @@ package op
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/caos/oidc/pkg/oidc"
|
"github.com/caos/oidc/pkg/oidc"
|
||||||
"github.com/caos/oidc/pkg/utils"
|
"github.com/caos/oidc/pkg/utils"
|
||||||
|
@ -16,6 +15,11 @@ type Introspector interface {
|
||||||
AccessTokenVerifier() AccessTokenVerifier
|
AccessTokenVerifier() AccessTokenVerifier
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type IntrospectorJWTProfile interface {
|
||||||
|
Introspector
|
||||||
|
JWTProfileVerifier() JWTProfileVerifier
|
||||||
|
}
|
||||||
|
|
||||||
func introspectionHandler(introspector Introspector) func(http.ResponseWriter, *http.Request) {
|
func introspectionHandler(introspector Introspector) func(http.ResponseWriter, *http.Request) {
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
Introspect(w, r, introspector)
|
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) {
|
func Introspect(w http.ResponseWriter, r *http.Request, introspector Introspector) {
|
||||||
callerToken := r.Header.Get("authorization")
|
|
||||||
response := oidc.NewIntrospectionResponse()
|
response := oidc.NewIntrospectionResponse()
|
||||||
callerToken, callerSubject, ok := getTokenIDAndSubject(r.Context(), introspector, strings.TrimPrefix(callerToken, oidc.PrefixBearer))
|
token, clientID, err := ParseTokenIntrospectionRequest(r, introspector)
|
||||||
if !ok {
|
|
||||||
utils.MarshalJSON(w, response)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
introspectionToken, err := ParseTokenInrospectionRequest(r, introspector.Decoder())
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
utils.MarshalJSON(w, response)
|
http.Error(w, err.Error(), http.StatusUnauthorized)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
tokenID, subject, ok := getTokenIDAndSubject(r.Context(), introspector, introspectionToken)
|
tokenID, subject, ok := getTokenIDAndSubject(r.Context(), introspector, token)
|
||||||
if !ok {
|
if !ok {
|
||||||
utils.MarshalJSON(w, response)
|
utils.MarshalJSON(w, response)
|
||||||
return
|
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 {
|
if err != nil {
|
||||||
utils.MarshalJSON(w, response)
|
utils.MarshalJSON(w, response)
|
||||||
return
|
return
|
||||||
|
@ -49,15 +47,31 @@ func Introspect(w http.ResponseWriter, r *http.Request, introspector Introspecto
|
||||||
utils.MarshalJSON(w, response)
|
utils.MarshalJSON(w, response)
|
||||||
}
|
}
|
||||||
|
|
||||||
func ParseTokenInrospectionRequest(r *http.Request, decoder utils.Decoder) (string, error) {
|
func ParseTokenIntrospectionRequest(r *http.Request, introspector Introspector) (token, clientID string, err error) {
|
||||||
err := r.ParseForm()
|
err = r.ParseForm()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", errors.New("unable to parse request")
|
return "", "", errors.New("unable to parse request")
|
||||||
}
|
}
|
||||||
req := new(oidc.IntrospectionRequest)
|
req := new(struct {
|
||||||
err = decoder.Decode(req, r.Form)
|
oidc.IntrospectionRequest
|
||||||
|
oidc.ClientAssertionParams
|
||||||
|
})
|
||||||
|
err = introspector.Decoder().Decode(req, r.Form)
|
||||||
if err != nil {
|
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) {
|
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 {
|
if err != nil {
|
||||||
http.Error(w, "access token missing", http.StatusUnauthorized)
|
http.Error(w, "access token missing", http.StatusUnauthorized)
|
||||||
return
|
return
|
||||||
|
@ -43,16 +43,12 @@ func Userinfo(w http.ResponseWriter, r *http.Request, userinfoProvider UserinfoP
|
||||||
utils.MarshalJSON(w, info)
|
utils.MarshalJSON(w, info)
|
||||||
}
|
}
|
||||||
|
|
||||||
func getAccessToken(r *http.Request, decoder utils.Decoder) (string, error) {
|
func ParseUserinfoRequest(r *http.Request, decoder utils.Decoder) (string, error) {
|
||||||
authHeader := r.Header.Get("authorization")
|
accessToken, err := getAccessToken(r)
|
||||||
if authHeader != "" {
|
if err == nil {
|
||||||
parts := strings.Split(authHeader, "Bearer ")
|
return accessToken, nil
|
||||||
if len(parts) != 2 {
|
|
||||||
return "", errors.New("invalid auth header")
|
|
||||||
}
|
}
|
||||||
return parts[1], nil
|
err = r.ParseForm()
|
||||||
}
|
|
||||||
err := r.ParseForm()
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", errors.New("unable to parse request")
|
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
|
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) {
|
func getTokenIDAndSubject(ctx context.Context, userinfoProvider UserinfoProvider, accessToken string) (string, string, bool) {
|
||||||
tokenIDSubject, err := userinfoProvider.Crypto().Decrypt(accessToken)
|
tokenIDSubject, err := userinfoProvider.Crypto().Decrypt(accessToken)
|
||||||
if err == nil {
|
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
|
//Discover calls the discovery endpoint of the provided issuer and returns the found endpoints
|
||||||
func Discover(issuer string, httpClient *http.Client) (Endpoints, error) {
|
func Discover(issuer string, httpClient *http.Client) (Endpoints, error) {
|
||||||
wellKnown := strings.TrimSuffix(issuer, "/") + oidc.DiscoveryEndpoint
|
wellKnown := strings.TrimSuffix(issuer, "/") + oidc.DiscoveryEndpoint
|
||||||
|
@ -327,14 +335,14 @@ func CodeExchangeHandler(callback func(http.ResponseWriter, *http.Request, *oidc
|
||||||
}
|
}
|
||||||
codeOpts = append(codeOpts, WithCodeVerifier(codeVerifier))
|
codeOpts = append(codeOpts, WithCodeVerifier(codeVerifier))
|
||||||
}
|
}
|
||||||
//if len(rp.ClientKey()) > 0 {
|
if len(rp.ClientKey()) > 0 {
|
||||||
// assertion, err := oidc.NewJWTProfileAssertionStringFromFileData(rp.ClientKey(), []string{rp.OAuthConfig().Endpoint.TokenURL})
|
assertion, err := oidc.GenerateJWTProfileToken(oidc.NewJWTProfileAssertion(rp.OAuthConfig().ClientID, rp.ClientKeyID(), []string{"http://localhost:50002/oauth/v2"}, rp.ClientKey()))
|
||||||
// if err != nil {
|
if err != nil {
|
||||||
// http.Error(w, "failed to build assertion: "+err.Error(), http.StatusUnauthorized)
|
http.Error(w, "failed to build assertion: "+err.Error(), http.StatusUnauthorized)
|
||||||
// return
|
return
|
||||||
// }
|
}
|
||||||
// codeOpts = append(codeOpts, WithClientAssertionJWT(assertion))
|
codeOpts = append(codeOpts, WithClientAssertionJWT(assertion))
|
||||||
//}
|
}
|
||||||
tokens, err := CodeExchange(r.Context(), params.Get("code"), rp, codeOpts...)
|
tokens, err := CodeExchange(r.Context(), params.Get("code"), rp, codeOpts...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, "failed to exchange token: "+err.Error(), http.StatusUnauthorized)
|
http.Error(w, "failed to exchange token: "+err.Error(), http.StatusUnauthorized)
|
||||||
|
|
|
@ -4,10 +4,11 @@ import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"time"
|
||||||
|
|
||||||
"golang.org/x/oauth2"
|
"golang.org/x/oauth2"
|
||||||
"golang.org/x/oauth2/clientcredentials"
|
"golang.org/x/oauth2/clientcredentials"
|
||||||
"golang.org/x/oauth2/jwt"
|
|
||||||
|
|
||||||
"github.com/caos/oidc/pkg/oidc"
|
"github.com/caos/oidc/pkg/oidc"
|
||||||
"github.com/caos/oidc/pkg/utils"
|
"github.com/caos/oidc/pkg/utils"
|
||||||
|
@ -16,6 +17,7 @@ import (
|
||||||
type ResourceServer interface {
|
type ResourceServer interface {
|
||||||
IntrospectionURL() string
|
IntrospectionURL() string
|
||||||
HttpClient() *http.Client
|
HttpClient() *http.Client
|
||||||
|
AuthFn() interface{}
|
||||||
}
|
}
|
||||||
|
|
||||||
type resourceServer struct {
|
type resourceServer struct {
|
||||||
|
@ -23,6 +25,32 @@ type resourceServer struct {
|
||||||
tokenURL string
|
tokenURL string
|
||||||
introspectURL string
|
introspectURL string
|
||||||
httpClient *http.Client
|
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 {
|
func (r *resourceServer) IntrospectionURL() string {
|
||||||
|
@ -33,8 +61,12 @@ func (r *resourceServer) HttpClient() *http.Client {
|
||||||
return r.httpClient
|
return r.httpClient
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *resourceServer) AuthFn() interface{} {
|
||||||
|
return r.authFn
|
||||||
|
}
|
||||||
|
|
||||||
func NewResourceServerClientCredentials(issuer, clientID, clientSecret string, option RSOption) (ResourceServer, error) {
|
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{
|
return (&clientcredentials.Config{
|
||||||
ClientID: clientID,
|
ClientID: clientID,
|
||||||
ClientSecret: clientSecret,
|
ClientSecret: clientSecret,
|
||||||
|
@ -44,20 +76,52 @@ func NewResourceServerClientCredentials(issuer, clientID, clientSecret string, o
|
||||||
return newResourceServer(issuer, authorizer, option)
|
return newResourceServer(issuer, authorizer, option)
|
||||||
}
|
}
|
||||||
func NewResourceServerJWTProfile(issuer, clientID, keyID string, key []byte, options ...RSOption) (ResourceServer, error) {
|
func NewResourceServerJWTProfile(issuer, clientID, keyID string, key []byte, options ...RSOption) (ResourceServer, error) {
|
||||||
authorizer := func(tokenURL string) func(ctx context.Context) *http.Client {
|
ts := &jwtAccessTokenSource{
|
||||||
return (&jwt.Config{
|
clientID: clientID,
|
||||||
Email: clientID,
|
|
||||||
Subject: clientID,
|
|
||||||
PrivateKey: key,
|
PrivateKey: key,
|
||||||
PrivateKeyID: keyID,
|
PrivateKeyID: keyID,
|
||||||
Audience: issuer,
|
audience: []string{issuer},
|
||||||
TokenURL: tokenURL,
|
|
||||||
}).Client
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//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...)
|
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{
|
rp := &resourceServer{
|
||||||
issuer: issuer,
|
issuer: issuer,
|
||||||
httpClient: utils.DefaultHTTPClient,
|
httpClient: utils.DefaultHTTPClient,
|
||||||
|
@ -76,7 +140,8 @@ func newResourceServer(issuer string, authorizer func(tokenURL string) func(ctx
|
||||||
if rp.introspectURL == "" || rp.tokenURL == "" {
|
if rp.introspectURL == "" || rp.tokenURL == "" {
|
||||||
return nil, errors.New("introspectURL and/or tokenURL is empty: please provide with either `WithStaticEndpoints` or a discovery url")
|
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
|
return rp, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -85,6 +150,7 @@ func NewResourceServerFromKeyFile(path string, options ...RSOption) (ResourceSer
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
c.Issuer = "http://localhost:50002/oauth/v2"
|
||||||
return NewResourceServerJWTProfile(c.Issuer, c.ClientID, c.KeyID, []byte(c.Key), options...)
|
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) {
|
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 {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
|
@ -42,6 +42,9 @@ func FormRequest(endpoint string, request interface{}, encoder Encoder, authFn i
|
||||||
if fn, ok := authFn.(FormAuthorization); ok {
|
if fn, ok := authFn.(FormAuthorization); ok {
|
||||||
fn(form)
|
fn(form)
|
||||||
}
|
}
|
||||||
|
if fn, ok := authFn.(func(url.Values)); ok {
|
||||||
|
fn(form)
|
||||||
|
}
|
||||||
body := strings.NewReader(form.Encode())
|
body := strings.NewReader(form.Encode())
|
||||||
req, err := http.NewRequest("POST", endpoint, body)
|
req, err := http.NewRequest("POST", endpoint, body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue