Merge branch 'master' into scopes

# Conflicts:
#	pkg/op/authrequest.go
#	pkg/op/authrequest_test.go
This commit is contained in:
Livio Amstutz 2020-09-07 09:39:28 +02:00
commit 007a68d861
20 changed files with 298 additions and 209 deletions

View file

@ -28,6 +28,10 @@ jobs:
uses: actions/checkout@v1
with:
fetch-depth: 1
- name: Create Version
uses: caos/semantic-release@v0.2.4
- name: Semantic Release
uses: cycjimmy/semantic-release-action@v2
with:
dry_run: false
semantic_version: 17.0.4
extra_plugins: |
@semantic-release/exec@5.0.0

View file

@ -195,14 +195,14 @@ func (s *AuthStorage) GetClientByClientID(_ context.Context, id string) (op.Clie
authMethod = op.AuthMethodNone
accessTokenType = op.AccessTokenTypeJWT
}
return &ConfClient{ID: id, applicationType: appType, authMethod: authMethod, accessTokenType: accessTokenType}, nil
return &ConfClient{ID: id, applicationType: appType, authMethod: authMethod, accessTokenType: accessTokenType, devMode: false}, nil
}
func (s *AuthStorage) AuthorizeClientIDSecret(_ context.Context, id string, _ string) error {
return nil
}
func (s *AuthStorage) GetUserinfoFromToken(ctx context.Context, _ string) (*oidc.Userinfo, error) {
func (s *AuthStorage) GetUserinfoFromToken(ctx context.Context, _, _ string) (*oidc.Userinfo, error) {
return s.GetUserinfoFromScopes(ctx, "", []string{})
}
func (s *AuthStorage) GetUserinfoFromScopes(_ context.Context, _ string, _ []string) (*oidc.Userinfo, error) {
@ -232,8 +232,10 @@ func (s *AuthStorage) GetUserinfoFromScopes(_ context.Context, _ string, _ []str
type ConfClient struct {
applicationType op.ApplicationType
authMethod op.AuthMethod
responseTypes []oidc.ResponseType
ID string
accessTokenType op.AccessTokenType
devMode bool
}
func (c *ConfClient) GetID() string {
@ -262,7 +264,7 @@ func (c *ConfClient) ApplicationType() op.ApplicationType {
return c.applicationType
}
func (c *ConfClient) GetAuthMethod() op.AuthMethod {
func (c *ConfClient) AuthMethod() op.AuthMethod {
return c.authMethod
}
@ -272,3 +274,10 @@ func (c *ConfClient) IDTokenLifetime() time.Duration {
func (c *ConfClient) AccessTokenType() op.AccessTokenType {
return c.accessTokenType
}
func (c *ConfClient) ResponseTypes() []oidc.ResponseType {
return c.responseTypes
}
func (c *ConfClient) DevMode() bool {
return c.devMode
}

6
go.mod
View file

@ -4,12 +4,12 @@ go 1.13
require (
github.com/caos/logging v0.0.0-20191210002624-b3260f690a6a
github.com/golang/mock v1.4.3
github.com/golang/mock v1.4.4
github.com/google/go-cmp v0.4.1 // indirect
github.com/google/go-github/v31 v31.0.0
github.com/google/uuid v1.1.1
github.com/google/uuid v1.1.2
github.com/gorilla/handlers v1.4.2
github.com/gorilla/mux v1.7.4
github.com/gorilla/mux v1.8.0
github.com/gorilla/schema v1.1.0
github.com/gorilla/securecookie v1.1.1
github.com/kr/pretty v0.1.0 // indirect

6
go.sum
View file

@ -6,6 +6,8 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/golang/mock v1.4.3 h1:GV+pQPG/EUUbkh47niozDcADz6go/dUwhVzdUQHIVRw=
github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/golang/mock v1.4.4 h1:l75CXGRSwbaYNpl/Z2X1XIIAMSCquvXgpVZDhwEIJsc=
github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs=
@ -18,10 +20,14 @@ github.com/google/go-querystring v1.0.0 h1:Xkwi/a1rcvNg1PPYe5vI8GbeBY/jrVuDX5ASu
github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck=
github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY=
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.1.2 h1:EVhdT+1Kseyi1/pUmXKaFxYsDNy9RQYkMWRH68J/W7Y=
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/handlers v1.4.2 h1:0QniY0USkHQ1RGCLfKxeNHK9bkDHGRYGNDFBCS+YARg=
github.com/gorilla/handlers v1.4.2/go.mod h1:Qkdc/uu4tH4g6mTK6auzZ766c4CA0Ng8+o/OAirnOIQ=
github.com/gorilla/mux v1.7.4 h1:VuZ8uybHlWmqV03+zRzdwKL4tUnIp1MAQtp1mIFE1bc=
github.com/gorilla/mux v1.7.4/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
github.com/gorilla/schema v1.1.0 h1:CamqUDOFUBqzrvxuz2vEwo8+SUdwsluFh7IlzJh30LY=
github.com/gorilla/schema v1.1.0/go.mod h1:kgLaKoK1FELgZqMAVxx/5cbj0kT+57qxUrAlIO2eleU=
github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ=

View file

@ -20,5 +20,6 @@ type DiscoveryConfiguration struct {
SubjectTypesSupported []string `json:"subject_types_supported,omitempty"`
IDTokenSigningAlgValuesSupported []string `json:"id_token_signing_alg_values_supported,omitempty"`
TokenEndpointAuthMethodsSupported []string `json:"token_endpoint_auth_methods_supported,omitempty"`
CodeChallengeMethodsSupported []string `json:"code_challenge_methods_supported,omitempty"`
ClaimsSupported []string `json:"claims_supported,omitempty"`
}

View file

@ -80,13 +80,17 @@ func ParseAuthorizeRequest(r *http.Request, decoder utils.Decoder) (*oidc.AuthRe
}
func ValidateAuthRequest(ctx context.Context, authReq *oidc.AuthRequest, storage Storage, verifier rp.Verifier) (string, error) {
client, err := storage.GetClientByClientID(ctx, authReq.ClientID)
if err != nil {
return "", ErrServerError(err.Error())
}
if err := ValidateAuthReqScopes(authReq.Scopes); err != nil {
return "", err
}
if err := ValidateAuthReqRedirectURI(ctx, authReq.RedirectURI, authReq.ClientID, authReq.ResponseType, storage); err != nil {
if err := ValidateAuthReqRedirectURI(client, authReq.RedirectURI, authReq.ResponseType); err != nil {
return "", err
}
if err := ValidateAuthReqResponseType(authReq.ResponseType); err != nil {
if err := ValidateAuthReqResponseType(client, authReq.ResponseType); err != nil {
return "", err
}
return ValidateAuthReqIDTokenHint(ctx, authReq.IDTokenHint, verifier)
@ -94,24 +98,23 @@ func ValidateAuthRequest(ctx context.Context, authReq *oidc.AuthRequest, storage
func ValidateAuthReqScopes(scopes []string) error {
if len(scopes) == 0 {
return ErrInvalidRequest("Unfortunately, the scope parameter of your request is missing. Please ensure your scope value is not empty, and try again. If you have any questions, you may contact the administrator of the application.")
return ErrInvalidRequest("The scope of your request is missing. Please ensure some scopes are requested. If you have any questions, you may contact the administrator of the application.")
}
if !utils.Contains(scopes, oidc.ScopeOpenID) {
return ErrInvalidRequest("Unfortunately, the scope `openid` is missing. Please ensure your scope configuration is correct (containing the `openid` value), and try again. If you have any questions, you may contact the administrator of the application.")
return ErrInvalidRequest("The scope openid is missing in your request. Please ensure the scope openid is added to the request. If you have any questions, you may contact the administrator of the application.")
}
return nil
}
func ValidateAuthReqRedirectURI(ctx context.Context, uri, clientID string, responseType oidc.ResponseType, storage OPStorage) error {
func ValidateAuthReqRedirectURI(client Client, uri string, responseType oidc.ResponseType) error {
if uri == "" {
return ErrInvalidRequestRedirectURI("Unfortunately, the client's redirect_uri is missing. Please ensure your redirect_uri is included in the request, and try again. If you have any questions, you may contact the administrator of the application.")
}
client, err := storage.GetClientByClientID(ctx, clientID)
if err != nil {
return ErrServerError(err.Error())
return ErrInvalidRequestRedirectURI("The redirect_uri is missing in the request. Please ensure it is added to the request. If you have any questions, you may contact the administrator of the application.")
}
if !utils.Contains(client.RedirectURIs(), uri) {
return ErrInvalidRequestRedirectURI("Unfortunately, the redirect_uri is missing in the client configuration. Please ensure your redirect_uri is added in the client configuration, and try again. If you have any questions, you may contact the administrator of the application.")
return ErrInvalidRequestRedirectURI("The requested redirect_uri is missing in the client configuration. If you have any questions, you may contact the administrator of the application.")
}
if client.DevMode() {
return nil
}
if strings.HasPrefix(uri, "https://") {
return nil
@ -120,32 +123,29 @@ func ValidateAuthReqRedirectURI(ctx context.Context, uri, clientID string, respo
if strings.HasPrefix(uri, "http://") && IsConfidentialType(client) {
return nil
}
if client.ApplicationType() == ApplicationTypeNative {
if !strings.HasPrefix(uri, "http://") && client.ApplicationType() == ApplicationTypeNative {
return nil
}
return ErrInvalidRequest("Unfortunately, this client's redirect_uri is http and is not allowed. If you have any questions, you may contact the administrator of the application.")
return ErrInvalidRequest("This client's redirect_uri is http and is not allowed. If you have any questions, you may contact the administrator of the application.")
} else {
if client.ApplicationType() != ApplicationTypeNative {
return ErrInvalidRequestRedirectURI("Unfortunately, http is only allowed for native applications. Please change your redirect uri configuration and try again. If you have any questions, you may contact the administrator of the application.")
return ErrInvalidRequestRedirectURI("Http is only allowed for native applications. Please change your redirect uri try again. If you have any questions, you may contact the administrator of the application.")
}
if !(strings.HasPrefix(uri, "http://localhost:") || strings.HasPrefix(uri, "http://localhost/")) {
return ErrInvalidRequestRedirectURI("Unfortunately, http is only allowed for localhost url. Please change your redirect uri configuration and try again. If you have any questions, you may contact the administrator of the application at:")
return ErrInvalidRequestRedirectURI("Http is only allowed for localhost uri. Please change your redirect uri try again. If you have any questions, you may contact the administrator of the application at:")
}
}
return nil
}
func ValidateAuthReqResponseType(responseType oidc.ResponseType) error {
switch responseType {
case oidc.ResponseTypeCode,
oidc.ResponseTypeIDToken,
oidc.ResponseTypeIDTokenOnly:
return nil
case "":
return ErrInvalidRequest("Unfortunately, the response type is missing in your request. Please ensure the response type is complete and accurate, and try again. If you have any questions, you may contact the administrator of the application.")
default:
return ErrInvalidRequest("Unfortunately, the response type provided in your request is invalid. Please ensure the response type is valid, and try again. If you have any questions, you may contact the administrator of the application.")
func ValidateAuthReqResponseType(client Client, responseType oidc.ResponseType) error {
if responseType == "" {
return ErrInvalidRequest("The response type is missing in your request. If you have any questions, you may contact the administrator of the application.")
}
if !ContainsResponseType(client.ResponseTypes(), responseType) {
return ErrInvalidRequest("The requested response type is missing in the client configuration. If you have any questions, you may contact the administrator of the application.")
}
return nil
}
func ValidateAuthReqIDTokenHint(ctx context.Context, idTokenHint string, verifier rp.Verifier) (string, error) {
@ -154,7 +154,7 @@ func ValidateAuthReqIDTokenHint(ctx context.Context, idTokenHint string, verifie
}
claims, err := verifier.VerifyIdToken(ctx, idTokenHint)
if err != nil {
return "", ErrInvalidRequest("Unfortunately, the id_token_hint is invalid. Please ensure the id_token_hint is complete and accurate, and try again. If you have any questions, you may contact the administrator of the application.")
return "", ErrInvalidRequest("The id_token_hint is invalid. If you have any questions, you may contact the administrator of the application.")
}
return claims.Subject, nil
}

View file

@ -158,27 +158,27 @@ func TestValidateAuthRequest(t *testing.T) {
// }
{
"scope missing fails",
args{&oidc.AuthRequest{}, nil, nil},
args{&oidc.AuthRequest{}, mock.NewMockStorageExpectValidClientID(t), nil},
true,
},
{
"scope openid missing fails",
args{&oidc.AuthRequest{Scopes: []string{"profile"}}, nil, nil},
args{&oidc.AuthRequest{Scopes: []string{"profile"}}, mock.NewMockStorageExpectValidClientID(t), nil},
true,
},
{
"response_type missing fails",
args{&oidc.AuthRequest{Scopes: []string{"openid"}}, nil, nil},
args{&oidc.AuthRequest{Scopes: []string{"openid"}}, mock.NewMockStorageExpectValidClientID(t), nil},
true,
},
{
"client_id missing fails",
args{&oidc.AuthRequest{Scopes: []string{"openid"}, ResponseType: oidc.ResponseTypeCode}, nil, nil},
args{&oidc.AuthRequest{Scopes: []string{"openid"}, ResponseType: oidc.ResponseTypeCode}, mock.NewMockStorageExpectValidClientID(t), nil},
true,
},
{
"redirect_uri missing fails",
args{&oidc.AuthRequest{Scopes: []string{"openid"}, ResponseType: oidc.ResponseTypeCode, ClientID: "client_id"}, nil, nil},
args{&oidc.AuthRequest{Scopes: []string{"openid"}, ResponseType: oidc.ResponseTypeCode, ClientID: "client_id"}, mock.NewMockStorageExpectValidClientID(t), nil},
true,
},
}
@ -223,9 +223,8 @@ func TestValidateAuthReqScopes(t *testing.T) {
func TestValidateAuthReqRedirectURI(t *testing.T) {
type args struct {
uri string
clientID string
client op.Client
responseType oidc.ResponseType
storage op.OPStorage
}
tests := []struct {
name string
@ -234,68 +233,106 @@ func TestValidateAuthReqRedirectURI(t *testing.T) {
}{
{
"empty fails",
args{"", "", oidc.ResponseTypeCode, nil},
args{"",
mock.NewClientWithConfig(t, []string{"https://registered.com/callback"}, op.ApplicationTypeWeb, nil, false),
oidc.ResponseTypeCode},
true,
},
{
"unregistered fails",
args{"https://unregistered.com/callback", "web_client", oidc.ResponseTypeCode, mock.NewMockStorageExpectValidClientID(t)},
true,
},
{
"storage error fails",
args{"https://registered.com/callback", "non_client", oidc.ResponseTypeIDToken, mock.NewMockStorageExpectInvalidClientID(t)},
args{"https://unregistered.com/callback",
mock.NewClientWithConfig(t, []string{"https://registered.com/callback"}, op.ApplicationTypeWeb, nil, false),
oidc.ResponseTypeCode},
true,
},
{
"code flow registered http not confidential fails",
args{"http://registered.com/callback", "useragent_client", oidc.ResponseTypeCode, mock.NewMockStorageExpectValidClientID(t)},
args{"http://registered.com/callback",
mock.NewClientWithConfig(t, []string{"http://registered.com/callback"}, op.ApplicationTypeUserAgent, nil, false),
oidc.ResponseTypeCode},
true,
},
{
"code flow registered http confidential ok",
args{"http://registered.com/callback", "web_client", oidc.ResponseTypeCode, mock.NewMockStorageExpectValidClientID(t)},
args{"http://registered.com/callback",
mock.NewClientWithConfig(t, []string{"http://registered.com/callback"}, op.ApplicationTypeWeb, nil, false),
oidc.ResponseTypeCode},
false,
},
{
"code flow registered custom not native fails",
args{"custom://callback", "useragent_client", oidc.ResponseTypeCode, mock.NewMockStorageExpectValidClientID(t)},
args{"custom://callback",
mock.NewClientWithConfig(t, []string{"custom://callback"}, op.ApplicationTypeUserAgent, nil, false),
oidc.ResponseTypeCode},
true,
},
{
"code flow registered custom native ok",
args{"http://registered.com/callback", "native_client", oidc.ResponseTypeCode, mock.NewMockStorageExpectValidClientID(t)},
args{"custom://callback",
mock.NewClientWithConfig(t, []string{"custom://callback"}, op.ApplicationTypeNative, nil, false),
oidc.ResponseTypeCode},
false,
},
{
"code flow dev mode http ok",
args{"http://registered.com/callback",
mock.NewClientWithConfig(t, []string{"http://registered.com/callback"}, op.ApplicationTypeNative, nil, true),
oidc.ResponseTypeCode},
false,
},
{
"implicit flow registered ok",
args{"https://registered.com/callback", "useragent_client", oidc.ResponseTypeIDToken, mock.NewMockStorageExpectValidClientID(t)},
args{"https://registered.com/callback",
mock.NewClientWithConfig(t, []string{"https://registered.com/callback"}, op.ApplicationTypeUserAgent, nil, false),
oidc.ResponseTypeIDToken},
false,
},
{
"implicit flow unregistered fails",
args{"https://unregistered.com/callback",
mock.NewClientWithConfig(t, []string{"https://registered.com/callback"}, op.ApplicationTypeUserAgent, nil, false),
oidc.ResponseTypeIDToken},
true,
},
{
"implicit flow registered http localhost native ok",
args{"http://localhost:9999/callback", "native_client", oidc.ResponseTypeIDToken, mock.NewMockStorageExpectValidClientID(t)},
args{"http://localhost:9999/callback",
mock.NewClientWithConfig(t, []string{"http://localhost:9999/callback"}, op.ApplicationTypeNative, nil, false),
oidc.ResponseTypeIDToken},
false,
},
{
"implicit flow registered http localhost user agent fails",
args{"http://localhost:9999/callback", "useragent_client", oidc.ResponseTypeIDToken, mock.NewMockStorageExpectValidClientID(t)},
args{"http://localhost:9999/callback",
mock.NewClientWithConfig(t, []string{"http://localhost:9999/callback"}, op.ApplicationTypeUserAgent, nil, false),
oidc.ResponseTypeIDToken},
true,
},
{
"implicit flow http non localhost fails",
args{"http://registered.com/callback", "native_client", oidc.ResponseTypeIDToken, mock.NewMockStorageExpectValidClientID(t)},
args{"http://registered.com/callback",
mock.NewClientWithConfig(t, []string{"http://registered.com/callback"}, op.ApplicationTypeNative, nil, false),
oidc.ResponseTypeIDToken},
true,
},
{
"implicit flow custom fails",
args{"custom://callback", "native_client", oidc.ResponseTypeIDToken, mock.NewMockStorageExpectValidClientID(t)},
args{"custom://callback",
mock.NewClientWithConfig(t, []string{"custom://callback"}, op.ApplicationTypeNative, nil, false),
oidc.ResponseTypeIDToken},
true,
},
{
"implicit flow dev mode http ok",
args{"http://registered.com/callback",
mock.NewClientWithConfig(t, []string{"http://registered.com/callback"}, op.ApplicationTypeNative, nil, true),
oidc.ResponseTypeIDToken},
false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if err := op.ValidateAuthReqRedirectURI(nil, tt.args.uri, tt.args.clientID, tt.args.responseType, tt.args.storage); (err != nil) != tt.wantErr {
if err := op.ValidateAuthReqRedirectURI(tt.args.client, tt.args.uri, tt.args.responseType); (err != nil) != tt.wantErr {
t.Errorf("ValidateRedirectURI() error = %v, wantErr %v", err.Error(), tt.wantErr)
}
})
@ -305,107 +342,36 @@ func TestValidateAuthReqRedirectURI(t *testing.T) {
func TestValidateAuthReqResponseType(t *testing.T) {
type args struct {
responseType oidc.ResponseType
}
type res struct {
err bool
client op.Client
}
tests := []struct {
name string
args args
res res
name string
args args
wantErr bool
}{
{
"code no error",
args{"code"},
res{false},
"empty response type",
args{"",
mock.NewClientWithConfig(t, nil, op.ApplicationTypeNative, []oidc.ResponseType{oidc.ResponseTypeCode}, true)},
true,
},
{
"id_token token no error",
args{"id_token token"},
res{false},
"response type missing in client config",
args{oidc.ResponseTypeIDToken,
mock.NewClientWithConfig(t, nil, op.ApplicationTypeNative, []oidc.ResponseType{oidc.ResponseTypeCode}, true)},
true,
},
{
"id_token no error",
args{"id_token"},
res{false},
},
{
"no response_type error",
args{},
res{true},
},
{
"invalid response_type error",
args{"invalid"},
res{true},
"valid response type",
args{oidc.ResponseTypeCode,
mock.NewClientWithConfig(t, nil, op.ApplicationTypeNative, []oidc.ResponseType{oidc.ResponseTypeCode}, true)},
false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if err := op.ValidateAuthReqResponseType(tt.args.responseType); (err != nil) != tt.res.err {
t.Errorf("ValidateAuthReqResponseType() error = %v, wantErr %v", err, tt.res.err)
}
})
}
}
func TestValidateAuthReqIDTokenHint(t *testing.T) {
type args struct {
idTokenHint string
verifier rp.Verifier
}
type res struct {
userID string
err bool
}
tests := []struct {
name string
args args
res res
}{
{
"no id_token_hint, no id and ok",
args{
"",
nil,
},
res{
"",
false,
},
},
{
"invalid id_token_hint, no id and error",
args{
"invalid",
rp_mock.NewMockVerifierExpectInvalid(t),
},
res{
"",
true,
},
},
{
"no id_token_hint ok",
args{
"valid",
rp_mock.NewMockVerifierExpectValid(t),
},
res{
"id",
false,
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := op.ValidateAuthReqIDTokenHint(nil, tt.args.idTokenHint, tt.args.verifier)
if (err != nil) != tt.res.err {
t.Errorf("ValidateAuthReqIDTokenHint() error = %v, wantErr %v", err, tt.res.err)
return
}
if got != tt.res.userID {
t.Errorf("ValidateAuthReqIDTokenHint() got = %v, want %v", got, tt.res.userID)
if err := op.ValidateAuthReqResponseType(tt.args.client, tt.args.responseType); (err != nil) != tt.wantErr {
t.Errorf("ValidateAuthReqScopes() error = %v, wantErr %v", err, tt.wantErr)
}
})
}

View file

@ -1,6 +1,9 @@
package op
import "time"
import (
"github.com/caos/oidc/pkg/oidc"
"time"
)
const (
ApplicationTypeWeb ApplicationType = iota
@ -16,16 +19,27 @@ type Client interface {
RedirectURIs() []string
PostLogoutRedirectURIs() []string
ApplicationType() ApplicationType
GetAuthMethod() AuthMethod
AuthMethod() AuthMethod
ResponseTypes() []oidc.ResponseType
LoginURL(string) string
AccessTokenType() AccessTokenType
IDTokenLifetime() time.Duration
DevMode() bool
}
func IsConfidentialType(c Client) bool {
return c.ApplicationType() == ApplicationTypeWeb
}
func ContainsResponseType(types []oidc.ResponseType, responseType oidc.ResponseType) bool {
for _, t := range types {
if t == responseType {
return true
}
}
return false
}
type ApplicationType int
type AuthMethod string

View file

@ -16,6 +16,7 @@ type Configuration interface {
KeysEndpoint() Endpoint
AuthMethodPostSupported() bool
CodeMethodS256Supported() bool
}
func ValidateIssuer(issuer string) error {

View file

@ -27,6 +27,8 @@ const (
AuthMethodBasic AuthMethod = "client_secret_basic"
AuthMethodPost = "client_secret_post"
AuthMethodNone = "none"
CodeMethodS256 = "S256"
)
var (
@ -41,24 +43,25 @@ var (
)
type DefaultOP struct {
config *Config
endpoints *endpoints
storage Storage
signer Signer
verifier rp.Verifier
crypto Crypto
http http.Handler
decoder *schema.Decoder
encoder *schema.Encoder
interceptor HttpInterceptor
retry func(int) (bool, int)
timer <-chan time.Time
config *Config
endpoints *endpoints
storage Storage
signer Signer
verifier rp.Verifier
crypto Crypto
http http.Handler
decoder *schema.Decoder
encoder *schema.Encoder
interceptors []HttpInterceptor
retry func(int) (bool, int)
timer <-chan time.Time
}
type Config struct {
Issuer string
CryptoKey [32]byte
DefaultLogoutRedirectURI string
CodeMethodS256 bool
// ScopesSupported: oidc.SupportedScopes,
// ResponseTypesSupported: responseTypes,
// GrantTypesSupported: oidc.SupportedGrantTypes,
@ -130,9 +133,9 @@ func WithCustomKeysEndpoint(endpoint Endpoint) DefaultOPOpts {
}
}
func WithHttpInterceptor(h HttpInterceptor) DefaultOPOpts {
func WithHttpInterceptors(interceptors ...HttpInterceptor) DefaultOPOpts {
return func(o *DefaultOP) error {
o.interceptor = h
o.interceptors = append(o.interceptors, interceptors...)
return nil
}
}
@ -183,7 +186,7 @@ func NewDefaultOP(ctx context.Context, config *Config, storage Storage, opOpts .
p.verifier = rp.NewDefaultVerifier(config.Issuer, "", p, rp.WithIgnoreAudience(), rp.WithIgnoreExpiration())
p.http = CreateRouter(p, p.interceptor)
p.http = CreateRouter(p, p.interceptors...)
p.decoder = schema.NewDecoder()
p.decoder.IgnoreUnknownKeys(true)
@ -223,6 +226,10 @@ func (p *DefaultOP) AuthMethodPostSupported() bool {
return true //TODO: config
}
func (p *DefaultOP) CodeMethodS256Supported() bool {
return p.config.CodeMethodS256
}
func (p *DefaultOP) HttpHandler() http.Handler {
return p.http
}

View file

@ -28,6 +28,7 @@ func CreateDiscoveryConfig(c Configuration, s Signer) *oidc.DiscoveryConfigurati
IDTokenSigningAlgValuesSupported: SigAlgorithms(s),
SubjectTypesSupported: SubjectTypes(c),
TokenEndpointAuthMethodsSupported: AuthMethods(c),
CodeChallengeMethodsSupported: CodeChallengeMethods(c),
}
}
@ -117,3 +118,11 @@ func AuthMethods(c Configuration) []string {
}
return authMethods
}
func CodeChallengeMethods(c Configuration) []string {
codeMethods := make([]string, 0, 1)
if c.CodeMethodS256Supported() {
codeMethods = append(codeMethods, CodeMethodS256)
}
return codeMethods
}

View file

@ -1,6 +1,7 @@
package mock
import (
"github.com/caos/oidc/pkg/oidc"
"testing"
gomock "github.com/golang/mock/gomock"
@ -27,3 +28,13 @@ func NewClientExpectAny(t *testing.T, appType op.ApplicationType) op.Client {
})
return c
}
func NewClientWithConfig(t *testing.T, uri []string, appType op.ApplicationType, responseTypes []oidc.ResponseType, devMode bool) op.Client {
c := NewClient(t)
m := c.(*MockClient)
m.EXPECT().RedirectURIs().AnyTimes().Return(uri)
m.EXPECT().ApplicationType().AnyTimes().Return(appType)
m.EXPECT().ResponseTypes().AnyTimes().Return(responseTypes)
m.EXPECT().DevMode().AnyTimes().Return(devMode)
return c
}

View file

@ -5,6 +5,7 @@
package mock
import (
oidc "github.com/caos/oidc/pkg/oidc"
op "github.com/caos/oidc/pkg/op"
gomock "github.com/golang/mock/gomock"
reflect "reflect"
@ -62,18 +63,32 @@ func (mr *MockClientMockRecorder) ApplicationType() *gomock.Call {
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ApplicationType", reflect.TypeOf((*MockClient)(nil).ApplicationType))
}
// GetAuthMethod mocks base method
func (m *MockClient) GetAuthMethod() op.AuthMethod {
// AuthMethod mocks base method
func (m *MockClient) AuthMethod() op.AuthMethod {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetAuthMethod")
ret := m.ctrl.Call(m, "AuthMethod")
ret0, _ := ret[0].(op.AuthMethod)
return ret0
}
// GetAuthMethod indicates an expected call of GetAuthMethod
func (mr *MockClientMockRecorder) GetAuthMethod() *gomock.Call {
// AuthMethod indicates an expected call of AuthMethod
func (mr *MockClientMockRecorder) AuthMethod() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAuthMethod", reflect.TypeOf((*MockClient)(nil).GetAuthMethod))
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AuthMethod", reflect.TypeOf((*MockClient)(nil).AuthMethod))
}
// DevMode mocks base method
func (m *MockClient) DevMode() bool {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "DevMode")
ret0, _ := ret[0].(bool)
return ret0
}
// DevMode indicates an expected call of DevMode
func (mr *MockClientMockRecorder) DevMode() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DevMode", reflect.TypeOf((*MockClient)(nil).DevMode))
}
// GetID mocks base method
@ -145,3 +160,17 @@ func (mr *MockClientMockRecorder) RedirectURIs() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RedirectURIs", reflect.TypeOf((*MockClient)(nil).RedirectURIs))
}
// ResponseTypes mocks base method
func (m *MockClient) ResponseTypes() []oidc.ResponseType {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ResponseTypes")
ret0, _ := ret[0].([]oidc.ResponseType)
return ret0
}
// ResponseTypes indicates an expected call of ResponseTypes
func (mr *MockClientMockRecorder) ResponseTypes() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ResponseTypes", reflect.TypeOf((*MockClient)(nil).ResponseTypes))
}

View file

@ -61,6 +61,20 @@ func (mr *MockConfigurationMockRecorder) AuthorizationEndpoint() *gomock.Call {
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AuthorizationEndpoint", reflect.TypeOf((*MockConfiguration)(nil).AuthorizationEndpoint))
}
// CodeMethodS256Supported mocks base method
func (m *MockConfiguration) CodeMethodS256Supported() bool {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "CodeMethodS256Supported")
ret0, _ := ret[0].(bool)
return ret0
}
// CodeMethodS256Supported indicates an expected call of CodeMethodS256Supported
func (mr *MockConfigurationMockRecorder) CodeMethodS256Supported() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CodeMethodS256Supported", reflect.TypeOf((*MockConfiguration)(nil).CodeMethodS256Supported))
}
// EndSessionEndpoint mocks base method
func (m *MockConfiguration) EndSessionEndpoint() op.Endpoint {
m.ctrl.T.Helper()
@ -103,20 +117,6 @@ func (mr *MockConfigurationMockRecorder) KeysEndpoint() *gomock.Call {
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "KeysEndpoint", reflect.TypeOf((*MockConfiguration)(nil).KeysEndpoint))
}
// Port mocks base method
func (m *MockConfiguration) Port() string {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Port")
ret0, _ := ret[0].(string)
return ret0
}
// Port indicates an expected call of Port
func (mr *MockConfigurationMockRecorder) Port() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Port", reflect.TypeOf((*MockConfiguration)(nil).Port))
}
// TokenEndpoint mocks base method
func (m *MockConfiguration) TokenEndpoint() op.Endpoint {
m.ctrl.T.Helper()

View file

@ -184,18 +184,18 @@ func (mr *MockStorageMockRecorder) GetUserinfoFromScopes(arg0, arg1, arg2 interf
}
// GetUserinfoFromToken mocks base method
func (m *MockStorage) GetUserinfoFromToken(arg0 context.Context, arg1 string) (*oidc.Userinfo, error) {
func (m *MockStorage) GetUserinfoFromToken(arg0 context.Context, arg1, arg2 string) (*oidc.Userinfo, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetUserinfoFromToken", arg0, arg1)
ret := m.ctrl.Call(m, "GetUserinfoFromToken", arg0, arg1, arg2)
ret0, _ := ret[0].(*oidc.Userinfo)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GetUserinfoFromToken indicates an expected call of GetUserinfoFromToken
func (mr *MockStorageMockRecorder) GetUserinfoFromToken(arg0, arg1 interface{}) *gomock.Call {
func (mr *MockStorageMockRecorder) GetUserinfoFromToken(arg0, arg1, arg2 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetUserinfoFromToken", reflect.TypeOf((*MockStorage)(nil).GetUserinfoFromToken), arg0, arg1)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetUserinfoFromToken", reflect.TypeOf((*MockStorage)(nil).GetUserinfoFromToken), arg0, arg1, arg2)
}
// Health mocks base method

View file

@ -3,6 +3,7 @@ package mock
import (
"context"
"errors"
"github.com/caos/oidc/pkg/oidc"
"testing"
"time"
@ -66,21 +67,25 @@ func ExpectValidClientID(s op.Storage) {
var appType op.ApplicationType
var authMethod op.AuthMethod
var accessTokenType op.AccessTokenType
var responseTypes []oidc.ResponseType
switch id {
case "web_client":
appType = op.ApplicationTypeWeb
authMethod = op.AuthMethodBasic
accessTokenType = op.AccessTokenTypeBearer
responseTypes = []oidc.ResponseType{oidc.ResponseTypeCode}
case "native_client":
appType = op.ApplicationTypeNative
authMethod = op.AuthMethodNone
accessTokenType = op.AccessTokenTypeBearer
responseTypes = []oidc.ResponseType{oidc.ResponseTypeCode}
case "useragent_client":
appType = op.ApplicationTypeUserAgent
authMethod = op.AuthMethodBasic
accessTokenType = op.AccessTokenTypeJWT
responseTypes = []oidc.ResponseType{oidc.ResponseTypeIDToken}
}
return &ConfClient{id: id, appType: appType, authMethod: authMethod, accessTokenType: accessTokenType}, nil
return &ConfClient{id: id, appType: appType, authMethod: authMethod, accessTokenType: accessTokenType, responseTypes: responseTypes}, nil
})
}
@ -116,6 +121,8 @@ type ConfClient struct {
appType op.ApplicationType
authMethod op.AuthMethod
accessTokenType op.AccessTokenType
responseTypes []oidc.ResponseType
devMode bool
}
func (c *ConfClient) RedirectURIs() []string {
@ -138,7 +145,7 @@ func (c *ConfClient) ApplicationType() op.ApplicationType {
return c.appType
}
func (c *ConfClient) GetAuthMethod() op.AuthMethod {
func (c *ConfClient) AuthMethod() op.AuthMethod {
return c.authMethod
}
@ -155,3 +162,9 @@ func (c *ConfClient) IDTokenLifetime() time.Duration {
func (c *ConfClient) AccessTokenType() op.AccessTokenType {
return c.accessTokenType
}
func (c *ConfClient) ResponseTypes() []oidc.ResponseType {
return c.responseTypes
}
func (c *ConfClient) DevMode() bool {
return c.devMode
}

View file

@ -27,28 +27,44 @@ type OpenIDProvider interface {
HttpHandler() http.Handler
}
type HttpInterceptor func(http.HandlerFunc) http.HandlerFunc
type HttpInterceptor func(http.Handler) http.Handler
var DefaultInterceptor = func(h http.HandlerFunc) http.HandlerFunc {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
h(w, r)
})
var allowAllOrigins = func(_ string) bool {
return true
}
func CreateRouter(o OpenIDProvider, h HttpInterceptor) *mux.Router {
if h == nil {
h = DefaultInterceptor
}
func CreateRouter(o OpenIDProvider, interceptors ...HttpInterceptor) *mux.Router {
intercept := buildInterceptor(interceptors...)
router := mux.NewRouter()
router.Use(handlers.CORS())
router.Use(handlers.CORS(
handlers.AllowCredentials(),
handlers.AllowedHeaders([]string{"authorization", "content-type"}),
handlers.AllowedOriginValidator(allowAllOrigins),
))
router.HandleFunc(healthzEndpoint, Healthz)
router.HandleFunc(readinessEndpoint, o.HandleReady)
router.HandleFunc(oidc.DiscoveryEndpoint, o.HandleDiscovery)
router.HandleFunc(o.AuthorizationEndpoint().Relative(), h(o.HandleAuthorize))
router.HandleFunc(o.AuthorizationEndpoint().Relative()+"/{id}", h(o.HandleAuthorizeCallback))
router.HandleFunc(o.TokenEndpoint().Relative(), h(o.HandleExchange))
router.Handle(o.AuthorizationEndpoint().Relative(), intercept(o.HandleAuthorize))
router.Handle(o.AuthorizationEndpoint().Relative()+"/{id}", intercept(o.HandleAuthorizeCallback))
router.Handle(o.TokenEndpoint().Relative(), intercept(o.HandleExchange))
router.HandleFunc(o.UserinfoEndpoint().Relative(), o.HandleUserinfo)
router.HandleFunc(o.EndSessionEndpoint().Relative(), h(o.HandleEndSession))
router.Handle(o.EndSessionEndpoint().Relative(), intercept(o.HandleEndSession))
router.HandleFunc(o.KeysEndpoint().Relative(), o.HandleKeys)
return router
}
func buildInterceptor(interceptors ...HttpInterceptor) func(http.HandlerFunc) http.Handler {
return func(handlerFunc http.HandlerFunc) http.Handler {
handler := handlerFuncToHandler(handlerFunc)
for i := len(interceptors) - 1; i >= 0; i-- {
handler = interceptors[i](handler)
}
return handler
}
}
func handlerFuncToHandler(handlerFunc http.HandlerFunc) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
handlerFunc(w, r)
})
}

View file

@ -29,7 +29,7 @@ type OPStorage interface {
GetClientByClientID(context.Context, string) (Client, error)
AuthorizeClientIDSecret(context.Context, string, string) error
GetUserinfoFromScopes(context.Context, string, []string) (*oidc.Userinfo, error)
GetUserinfoFromToken(context.Context, string) (*oidc.Userinfo, error)
GetUserinfoFromToken(context.Context, string, string) (*oidc.Userinfo, error)
}
type Storage interface {

View file

@ -78,11 +78,11 @@ func AuthorizeClient(ctx context.Context, tokenReq *oidc.AccessTokenRequest, exc
if err != nil {
return nil, nil, err
}
if client.GetAuthMethod() == AuthMethodNone {
if client.AuthMethod() == AuthMethodNone {
authReq, err := AuthorizeCodeChallenge(ctx, tokenReq, exchanger)
return authReq, client, err
}
if client.GetAuthMethod() == AuthMethodPost && !exchanger.AuthMethodPostSupported() {
if client.AuthMethod() == AuthMethodPost && !exchanger.AuthMethodPostSupported() {
return nil, nil, errors.New("basic not supported")
}
err = AuthorizeClientIDSecret(ctx, tokenReq.ClientID, tokenReq.ClientSecret, exchanger.Storage())

View file

@ -5,6 +5,8 @@ import (
"net/http"
"strings"
"github.com/gorilla/schema"
"github.com/caos/oidc/pkg/oidc"
"github.com/caos/oidc/pkg/utils"
)
@ -26,8 +28,9 @@ func Userinfo(w http.ResponseWriter, r *http.Request, userinfoProvider UserinfoP
http.Error(w, "access token missing", http.StatusUnauthorized)
return
}
info, err := userinfoProvider.Storage().GetUserinfoFromToken(r.Context(), tokenID)
info, err := userinfoProvider.Storage().GetUserinfoFromToken(r.Context(), tokenID, r.Header.Get("origin"))
if err != nil {
w.WriteHeader(http.StatusForbidden)
utils.MarshalJSON(w, err)
return
}