Some checks failed
CI / Run tests and lints (push) Failing after 29s
CI / Build CLI docs (push) Has been skipped
CI / Run Playwright end-to-end tests (mysql) (push) Failing after 3s
CI / Run Playwright end-to-end tests (postgres) (push) Failing after 2s
CI / Run Playwright end-to-end tests (sqlite) (push) Failing after 3s
CodeQL / Analyze (go) (push) Failing after 4s
CodeQL / Analyze (javascript) (push) Failing after 3s
Docker Image Scanners / scanners (push) Failing after 4s
Synchronize Issue Labels / Synchronize Issue Labels (push) Has been skipped
CI / Run end-to-end tests (postgres) (push) Failing after 19s
CI / Run end-to-end tests (sqlite) (push) Failing after 2s
CI / Run Playwright end-to-end tests (cockroach) (push) Failing after 2s
CI / Draft newsletter (push) Has been skipped
CI / Generate release (push) Has been skipped
CI / Pending approval Slack notification (push) Has been skipped
CI / Send newsletter (push) Has been skipped
381 lines
15 KiB
Go
381 lines
15 KiB
Go
// Copyright © 2023 Ory Corp
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
|
|
package oidc_test
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"net/http"
|
|
"net/url"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/hashicorp/go-retryablehttp"
|
|
"github.com/jarcoal/httpmock"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
"golang.org/x/oauth2"
|
|
|
|
"github.com/ory/herodot"
|
|
"github.com/ory/kratos/driver"
|
|
"github.com/ory/kratos/driver/config"
|
|
"github.com/ory/kratos/pkg"
|
|
"github.com/ory/kratos/selfservice/strategy/oidc"
|
|
"github.com/ory/x/configx"
|
|
"github.com/ory/x/httpx"
|
|
)
|
|
|
|
type mockRegistry struct {
|
|
*driver.RegistryDefault
|
|
cl *retryablehttp.Client
|
|
}
|
|
|
|
func (s *mockRegistry) HTTPClient(context.Context, ...httpx.ResilientOptions) *retryablehttp.Client {
|
|
return s.cl
|
|
}
|
|
|
|
func TestProviderClaimsRespectsErrorCodes(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
_, base := pkg.NewFastRegistryWithMocks(t,
|
|
configx.WithValue(config.ViperKeyClientHTTPNoPrivateIPRanges, true))
|
|
reg := &mockRegistry{base, retryablehttp.NewClient()}
|
|
|
|
token := &oauth2.Token{AccessToken: "foo", Expiry: time.Now().Add(time.Hour)}
|
|
|
|
expectedClaims := &oidc.Claims{
|
|
Issuer: "ignore-me",
|
|
Subject: "123456789012345",
|
|
Name: "John Doe",
|
|
GivenName: "John",
|
|
FamilyName: "Doe",
|
|
Nickname: "John Doe",
|
|
PreferredUsername: "John Doe",
|
|
Email: "john.doe@example.com",
|
|
EmailVerified: true,
|
|
Birthdate: "01/01/1990",
|
|
}
|
|
|
|
defaultUserinfoHandler := func(req *http.Request) (*http.Response, error) {
|
|
if head := req.Header.Get("Authorization"); len(head) == 0 {
|
|
resp, err := httpmock.NewJsonResponse(401, map[string]interface{}{"error": ""})
|
|
return resp, err
|
|
}
|
|
|
|
resp, err := httpmock.NewJsonResponse(200, expectedClaims)
|
|
return resp, err
|
|
}
|
|
|
|
for _, tc := range []struct {
|
|
name string
|
|
issuer string
|
|
userInfoEndpoint string
|
|
config *oidc.Configuration
|
|
provider oidc.Provider
|
|
userInfoHandler func(req *http.Request) (*http.Response, error)
|
|
expectedClaims *oidc.Claims
|
|
useToken *oauth2.Token
|
|
hook func(t *testing.T)
|
|
}{
|
|
{
|
|
name: "auth0",
|
|
userInfoHandler: defaultUserinfoHandler,
|
|
userInfoEndpoint: "https://www.auth0.com/userinfo",
|
|
provider: oidc.NewProviderAuth0(&oidc.Configuration{
|
|
IssuerURL: "https://www.auth0.com",
|
|
ID: "auth0",
|
|
Provider: "auth0",
|
|
}, reg),
|
|
},
|
|
{
|
|
name: "salesforce",
|
|
userInfoHandler: defaultUserinfoHandler,
|
|
userInfoEndpoint: "https://www.salesforce.com/services/oauth2/userinfo",
|
|
provider: oidc.NewProviderSalesforce(&oidc.Configuration{
|
|
IssuerURL: "https://www.salesforce.com",
|
|
ID: "salesforce",
|
|
Provider: "salesforce",
|
|
}, reg),
|
|
},
|
|
{
|
|
name: "netid",
|
|
userInfoHandler: defaultUserinfoHandler,
|
|
userInfoEndpoint: "https://broker.netid.de/userinfo",
|
|
provider: oidc.NewProviderNetID(&oidc.Configuration{
|
|
IssuerURL: "https://broker.netid.de",
|
|
ID: "netid",
|
|
Provider: "netid",
|
|
ClientID: "foo",
|
|
}, reg),
|
|
useToken: token.WithExtra(map[string]interface{}{"id_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiZW1haWwiOiJqb2huLmRvZUBleGFtcGxlLmNvbSIsImlhdCI6MTUxNjIzOTAyMiwidGlkIjoiYTliODYzODUtZjMyYy00ODAzLWFmYzgtNGIyMzEyZmJkZjI0IiwiaXNzIjoiaHR0cHM6Ly9icm9rZXIubmV0aWQuZGUvIiwiYXVkIjpbImZvbyJdLCJleHAiOjQwNzE3Mjg1MDR9.Zt_-9jULoENQ7pq6rKrevhecBlWKR2rzNti42EJti_OelCrGbl5zyHYRfIg264VzEY0Zp9BAZTWmcF6Z-cpuD05RddTXZDrrC_47bJeYDL-bjDfKjoSZUt_1JnNFgqeyMA1Ji6HCIsEf_g8QnuSELAO0c-qb2ANmDLVL8_dY6oUmCN5oWLJS2q6xO-Iaz-AuKaGTbLZBcjCe2NB_--kIx4IrgLm78U90X3ePOV0CcYZLNvDgzEsVJ2M4ixKp87bYUaZZ3JJEjuxgnHqKRMExDKron3mvfQtC1L-dZQyeDo3-mCFJL_JhEw9zLmoWFBD7aARrVL_yAe31o26FB3S-Dg"}),
|
|
hook: func(t *testing.T) {
|
|
httpmock.RegisterResponder("GET", "https://broker.netid.de/.well-known/openid-configuration",
|
|
func(req *http.Request) (*http.Response, error) {
|
|
return httpmock.NewJsonResponse(200, map[string]interface{}{
|
|
"issuer": "https://broker.netid.de/",
|
|
"jwks_uri": "https://broker.netid.de/.well-known/jwks.json",
|
|
})
|
|
},
|
|
)
|
|
httpmock.RegisterResponder("GET", "https://broker.netid.de/.well-known/jwks.json",
|
|
func(req *http.Request) (*http.Response, error) {
|
|
return httpmock.NewJsonResponse(200, json.RawMessage(`{
|
|
"keys": [
|
|
{
|
|
"kty": "RSA",
|
|
"e": "AQAB",
|
|
"alg": "RS256",
|
|
"use": "sig",
|
|
"n": "uw0hrmZpA5CH5lDnjbCK6ZQD-I0BGxde-ChDzUz5eK4r7VtyMGvAxwD-k-mWw4FJ2NgYmT_T89sAtE6NQqT5u9HAe-CI22lf5LMvmqvMzekcmBAvXNw8VeTV_N6CbS9INJrxf20cObf-kpTxVxlYtYxYwIhYdOw3DwX8y31vI38qHQ4_OzTRo4KFVLCr68MzIHHRI4d5EHrFv1VFjiaa_ATwuwCMUfg0RMnK09FpMCgvp0E6bQeptXhBBNQVQkoC5whT1GzikfSxyeugjQ_XuTt1MKoyYsN2pmfrBdcfWrPYvV_JPgO1MkEtqErvtCByairINfXrHTMOxNWe3sYlXQ"
|
|
}
|
|
]
|
|
}`))
|
|
},
|
|
)
|
|
},
|
|
expectedClaims: &oidc.Claims{Issuer: "https://broker.netid.de/", Subject: "1234567890", Name: "John Doe", GivenName: "John", FamilyName: "Doe", LastName: "", MiddleName: "", Nickname: "John Doe", PreferredUsername: "John Doe", Profile: "", Picture: "", Website: "", Email: "john.doe@example.com", EmailVerified: true, Gender: "", Birthdate: "01/01/1990", Zoneinfo: "", Locale: "", PhoneNumber: "", PhoneNumberVerified: false, UpdatedAt: 0, HD: "", Team: ""},
|
|
},
|
|
{
|
|
name: "vk",
|
|
userInfoEndpoint: "https://api.vk.com/method/users.get",
|
|
provider: oidc.NewProviderVK(&oidc.Configuration{
|
|
IssuerURL: "https://oauth.vk.com",
|
|
ID: "vk",
|
|
Provider: "vk",
|
|
}, reg),
|
|
useToken: token.WithExtra(map[string]interface{}{"email": "john.doe@example.com"}),
|
|
userInfoHandler: func(req *http.Request) (*http.Response, error) {
|
|
if head := req.URL.Query().Get("access_token"); len(head) == 0 {
|
|
resp, err := httpmock.NewJsonResponse(401, map[string]interface{}{"error": ""})
|
|
return resp, err
|
|
}
|
|
|
|
resp, err := httpmock.NewJsonResponse(200, map[string]interface{}{
|
|
"response": []map[string]interface{}{{"id": 123456789012345}},
|
|
})
|
|
return resp, err
|
|
},
|
|
|
|
expectedClaims: &oidc.Claims{
|
|
Issuer: "https://api.vk.com/method/users.get",
|
|
Subject: "123456789012345",
|
|
Email: "john.doe@example.com",
|
|
},
|
|
},
|
|
{
|
|
name: "yandex",
|
|
userInfoEndpoint: "https://login.yandex.ru/info",
|
|
provider: oidc.NewProviderYandex(&oidc.Configuration{
|
|
IssuerURL: "https://oauth.yandex.com",
|
|
ID: "vk",
|
|
Provider: "vk",
|
|
}, reg),
|
|
useToken: token.WithExtra(map[string]interface{}{"email": "john.doe@example.com"}),
|
|
userInfoHandler: func(req *http.Request) (*http.Response, error) {
|
|
if head := req.URL.Query().Get("oauth_token"); len(head) == 0 {
|
|
resp, err := httpmock.NewJsonResponse(401, map[string]interface{}{"error": ""})
|
|
return resp, err
|
|
}
|
|
|
|
resp, err := httpmock.NewJsonResponse(200, map[string]interface{}{
|
|
"id": "123456789012345",
|
|
"default_email": "john.doe@example.com",
|
|
})
|
|
return resp, err
|
|
},
|
|
|
|
expectedClaims: &oidc.Claims{
|
|
Issuer: "https://login.yandex.ru/info",
|
|
Subject: "123456789012345",
|
|
Email: "john.doe@example.com",
|
|
Picture: "https://avatars.yandex.net/get-yapic//islands-200",
|
|
},
|
|
},
|
|
{
|
|
name: "facebook",
|
|
hook: func(t *testing.T) {
|
|
httpmock.RegisterResponder("GET", "https://www.facebook.com/.well-known/openid-configuration",
|
|
func(req *http.Request) (*http.Response, error) {
|
|
resp, err := httpmock.NewJsonResponse(200, map[string]interface{}{
|
|
"issuer": "https://www.facebook.com",
|
|
})
|
|
return resp, err
|
|
},
|
|
)
|
|
},
|
|
userInfoEndpoint: "https://graph.facebook.com/me",
|
|
provider: oidc.NewProviderFacebook(&oidc.Configuration{
|
|
ID: "facebook",
|
|
Provider: "facebook",
|
|
}, reg),
|
|
useToken: token,
|
|
userInfoHandler: func(req *http.Request) (*http.Response, error) {
|
|
if head := req.Header.Get("Authorization"); len(head) == 0 {
|
|
resp, err := httpmock.NewJsonResponse(401, map[string]interface{}{"error": ""})
|
|
return resp, err
|
|
}
|
|
if _, ok := req.URL.Query()["appsecret_proof"]; !ok {
|
|
resp, err := httpmock.NewJsonResponse(400, map[string]interface{}{"error": ""})
|
|
return resp, err
|
|
}
|
|
|
|
resp, err := httpmock.NewJsonResponse(200, map[string]interface{}{
|
|
"id": "123456789012345",
|
|
"name": "John Doe",
|
|
"first_name": "John",
|
|
"last_name": "Doe",
|
|
"email": "john.doe@example.com",
|
|
"birthday": "01/01/1990",
|
|
})
|
|
return resp, err
|
|
},
|
|
expectedClaims: &oidc.Claims{
|
|
Issuer: "https://graph.facebook.com/me?fields=id,name,first_name,last_name,middle_name,email,picture,birthday,gender&appsecret_proof=0c0d98f7e3d9d45e72e8877bc1b104327efb9c07b18f2ffeced76d81307f1fff",
|
|
Subject: "123456789012345",
|
|
Name: "John Doe",
|
|
GivenName: "John",
|
|
FamilyName: "Doe",
|
|
Nickname: "John Doe",
|
|
PreferredUsername: "John Doe",
|
|
Email: "john.doe@example.com",
|
|
EmailVerified: true,
|
|
Birthdate: "01/01/1990",
|
|
},
|
|
},
|
|
{
|
|
name: "gitlab",
|
|
userInfoHandler: defaultUserinfoHandler,
|
|
userInfoEndpoint: "https://www.gitlab.com/oauth/userinfo",
|
|
provider: oidc.NewProviderGitLab(&oidc.Configuration{
|
|
IssuerURL: "https://www.gitlab.com",
|
|
ID: "gitlab",
|
|
Provider: "gitlab",
|
|
}, reg),
|
|
},
|
|
// Microsoft is a more complicated set up because it actually verifies the ID Token before using the userinfo endpoint.
|
|
{
|
|
name: "microsoft",
|
|
userInfoHandler: func(req *http.Request) (*http.Response, error) {
|
|
if head := req.Header.Get("Authorization"); len(head) == 0 {
|
|
resp, err := httpmock.NewJsonResponse(401, map[string]interface{}{"error": ""})
|
|
return resp, err
|
|
}
|
|
|
|
resp, err := httpmock.NewJsonResponse(200, json.RawMessage(`{"id":"new-id"}`))
|
|
return resp, err
|
|
},
|
|
userInfoEndpoint: "https://graph.microsoft.com/v1.0/me",
|
|
provider: oidc.NewProviderMicrosoft(&oidc.Configuration{
|
|
IssuerURL: "https://login.microsoftonline.com/",
|
|
ID: "microsoft",
|
|
Provider: "microsoft",
|
|
Tenant: "a9b86385-f32c-4803-afc8-4b2312fbdf24",
|
|
ClientID: "foo",
|
|
ClientSecret: "bar",
|
|
SubjectSource: "me",
|
|
}, reg),
|
|
useToken: token.WithExtra(map[string]interface{}{"id_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiZW1haWwiOiJqb2huLmRvZUBleGFtcGxlLmNvbSIsImlhdCI6MTUxNjIzOTAyMiwidGlkIjoiYTliODYzODUtZjMyYy00ODAzLWFmYzgtNGIyMzEyZmJkZjI0IiwiaXNzIjoiaHR0cHM6Ly9sb2dpbi5taWNyb3NvZnRvbmxpbmUuY29tL2E5Yjg2Mzg1LWYzMmMtNDgwMy1hZmM4LTRiMjMxMmZiZGYyNC92Mi4wIiwiYXVkIjpbImZvbyJdLCJleHAiOjQwNzE3Mjg1MDR9.LRgolO5_-26uMrBo89NCzUfi87a8jf7rXlWgZVYnpfowzqn-U0JhNGVzEOXOACoPdX9HsEtYj4hZxYgYcd6z7yqgmOZXE-y58L5BHYZU1kk37O1Dl_VnN-BmeCWs_JZiXF2KEnu7BW8btYnb26qCnc3_8RGbyJwI4UU6ynbJzAmLCPUPgoMZ2Jpahx_K8vqLe-4rveLyUvVVHMoUAV16I-Wg08GcuA5cY0_91-QpA5Kq0AA58wbrUbAOEAOYOpa_63QqYcCcZLbnX_w09Z1YCXGGVSbbpzjr3cJ8EFA6doloBKRLWtNtEnGk4hyjHyp_ls89ZYqJ1ngy95AEswzwJQ"}),
|
|
hook: func(t *testing.T) {
|
|
httpmock.RegisterResponder("GET", "https://login.microsoftonline.com/a9b86385-f32c-4803-afc8-4b2312fbdf24/v2.0/.well-known/openid-configuration",
|
|
func(req *http.Request) (*http.Response, error) {
|
|
return httpmock.NewJsonResponse(200, map[string]interface{}{
|
|
"issuer": "https://login.microsoftonline.com/a9b86385-f32c-4803-afc8-4b2312fbdf24/v2.0",
|
|
"jwks_uri": "https://login.microsoftonline.com/a9b86385-f32c-4803-afc8-4b2312fbdf24/v2.0/.well-known/jwks.json",
|
|
})
|
|
},
|
|
)
|
|
httpmock.RegisterResponder("GET", "https://login.microsoftonline.com/a9b86385-f32c-4803-afc8-4b2312fbdf24/v2.0/.well-known/jwks.json",
|
|
func(req *http.Request) (*http.Response, error) {
|
|
return httpmock.NewJsonResponse(200, json.RawMessage(`{
|
|
"keys": [
|
|
{
|
|
"kty": "RSA",
|
|
"e": "AQAB",
|
|
"use": "sig",
|
|
"alg": "RS256",
|
|
"n": "xGnYx5u9S7LCrtINTI1Kav6pqzFj72JhYunqrDhnG18Iu-MJdCSmd26xiiZFY6UsJoG-te_DcUE_YHvhx6vwC0tJO-Z2uxwKRxWsMiyW8v3V0Bfbtu7jlf0kpQYeAeAIAHih7GU_v5jtxaGLNR9JIbmENUGOoIfydfTWLKHtwI0MB8tNVwjqP8e6ZSr6DNIjsKKuz98BeRlZ576jyH2GcmM9DguLZv73qU1m1OLoHFW5rAPH70-nhr9V67TTR1A1is9v85uLlKBnZgKp-EdYtBEvKRWSpw5vxCnKggJjEYATOB2H5m_c8F1e1tsJLAehaXKMKZYI-VlWWxj7KixDKw"
|
|
}
|
|
]
|
|
}`))
|
|
},
|
|
)
|
|
},
|
|
expectedClaims: &oidc.Claims{
|
|
Issuer: "https://login.microsoftonline.com/a9b86385-f32c-4803-afc8-4b2312fbdf24/v2.0", Subject: "new-id", Name: "John Doe", Email: "john.doe@example.com",
|
|
RawClaims: map[string]interface{}{"aud": []interface{}{"foo"}, "exp": 4.071728504e+09, "iat": 1.516239022e+09, "iss": "https://login.microsoftonline.com/a9b86385-f32c-4803-afc8-4b2312fbdf24/v2.0", "email": "john.doe@example.com", "name": "John Doe", "sub": "1234567890", "tid": "a9b86385-f32c-4803-afc8-4b2312fbdf24"},
|
|
},
|
|
},
|
|
{
|
|
name: "dingtalk",
|
|
userInfoHandler: func(req *http.Request) (*http.Response, error) {
|
|
if head := req.Header.Get("x-acs-dingtalk-access-token"); len(head) == 0 {
|
|
resp, err := httpmock.NewJsonResponse(401, map[string]interface{}{"error": ""})
|
|
return resp, err
|
|
}
|
|
|
|
resp, err := httpmock.NewJsonResponse(200, map[string]interface{}{
|
|
"openId": "123456789012345",
|
|
"email": "john.doe@example.com",
|
|
})
|
|
return resp, err
|
|
},
|
|
userInfoEndpoint: "https://api.dingtalk.com/v1.0/contact/users/me",
|
|
provider: oidc.NewProviderDingTalk(&oidc.Configuration{
|
|
IssuerURL: "https://www.dingtalk.com",
|
|
ID: "dingtalk",
|
|
Provider: "dingtalk",
|
|
}, reg),
|
|
expectedClaims: &oidc.Claims{
|
|
Issuer: "https://api.dingtalk.com/v1.0/contact/users/me",
|
|
Subject: "123456789012345",
|
|
Email: "john.doe@example.com",
|
|
},
|
|
},
|
|
} {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
token := token
|
|
if tc.useToken != nil {
|
|
token = tc.useToken
|
|
}
|
|
|
|
t.Run("http error is respected", func(t *testing.T) {
|
|
httpmock.ActivateNonDefault(reg.cl.HTTPClient)
|
|
t.Cleanup(httpmock.DeactivateAndReset)
|
|
|
|
if tc.hook != nil {
|
|
tc.hook(t)
|
|
}
|
|
|
|
httpmock.RegisterResponder("GET", tc.userInfoEndpoint, func(req *http.Request) (*http.Response, error) {
|
|
return httpmock.NewJsonResponse(455, map[string]interface{}{})
|
|
})
|
|
|
|
_, err := tc.provider.(oidc.OAuth2Provider).Claims(t.Context(), token, url.Values{})
|
|
var he *herodot.DefaultError
|
|
require.ErrorAs(t, err, &he)
|
|
assert.Equal(t, "OpenID Connect provider returned a 455 status code but 200 is expected.", he.Reason(), "%+v", err)
|
|
})
|
|
|
|
t.Run("call is successful", func(t *testing.T) {
|
|
httpmock.ActivateNonDefault(reg.cl.HTTPClient)
|
|
t.Cleanup(httpmock.DeactivateAndReset)
|
|
|
|
if tc.hook != nil {
|
|
tc.hook(t)
|
|
}
|
|
|
|
httpmock.RegisterResponder("GET", tc.userInfoEndpoint, tc.userInfoHandler)
|
|
|
|
claims, err := tc.provider.(oidc.OAuth2Provider).Claims(t.Context(), token, url.Values{})
|
|
require.NoError(t, err)
|
|
if tc.expectedClaims == nil {
|
|
assert.Equal(t, expectedClaims, claims)
|
|
} else {
|
|
assert.Equal(t, tc.expectedClaims, claims)
|
|
}
|
|
})
|
|
})
|
|
}
|
|
}
|