oauth2-proxy/providers/oidc_test.go

716 lines
23 KiB
Go

package providers
import (
"context"
"encoding/base64"
"encoding/json"
"fmt"
"net/http"
"net/http/httptest"
"net/url"
"testing"
"github.com/coreos/go-oidc/v3/oidc"
"github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/options"
"github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/sessions"
"github.com/oauth2-proxy/oauth2-proxy/v7/pkg/encryption"
internaloidc "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/providers/oidc"
"github.com/stretchr/testify/assert"
)
type redeemTokenResponse struct {
AccessToken string `json:"access_token"`
RefreshToken string `json:"refresh_token"`
ExpiresIn int64 `json:"expires_in"`
TokenType string `json:"token_type"`
IDToken string `json:"id_token,omitempty"`
}
func newOIDCProvider(serverURL *url.URL, skipNonce bool) *OIDCProvider {
verificationOptions := internaloidc.IDTokenVerificationOptions{
AudienceClaims: []string{"aud"},
ClientID: "https://test.myapp.com",
}
providerData := &ProviderData{
ProviderName: "oidc",
ClientID: oidcClientID,
ClientSecret: oidcSecret,
LoginURL: &url.URL{
Scheme: serverURL.Scheme,
Host: serverURL.Host,
Path: "/login/oauth/authorize"},
RedeemURL: &url.URL{
Scheme: serverURL.Scheme,
Host: serverURL.Host,
Path: "/login/oauth/access_token"},
ProfileURL: &url.URL{
Scheme: serverURL.Scheme,
Host: serverURL.Host,
Path: "/profile"},
IntrospectURL: &url.URL{
Scheme: serverURL.Scheme,
Host: serverURL.Host,
Path: "/introspect"},
ValidateURL: &url.URL{
Scheme: serverURL.Scheme,
Host: serverURL.Host,
Path: "/api"},
Scope: "openid profile offline_access",
EmailClaim: "email",
GroupsClaim: "groups",
UserClaim: "sub",
Verifier: internaloidc.NewVerifier(oidc.NewVerifier(
oidcIssuer,
mockJWKS{},
&oidc.Config{ClientID: oidcClientID},
), verificationOptions),
}
p := NewOIDCProvider(providerData, options.OIDCOptions{
InsecureSkipNonce: skipNonce,
})
return p
}
func newOIDCServer(redeemJSON []byte, profileJSON []byte, introspectJSON []byte) (*url.URL, *httptest.Server) {
mux := http.NewServeMux()
if len(redeemJSON) > 0 {
mux.HandleFunc("/login/oauth/access_token", func(rw http.ResponseWriter, req *http.Request) {
rw.Header().Add("content-type", "application/json")
_, _ = rw.Write(redeemJSON)
})
}
if len(profileJSON) > 0 {
mux.HandleFunc("/profile", func(rw http.ResponseWriter, req *http.Request) {
rw.Header().Add("content-type", "application/json")
_, _ = rw.Write(profileJSON)
})
}
if len(introspectJSON) > 0 {
mux.HandleFunc("/introspect", func(rw http.ResponseWriter, req *http.Request) {
rw.Header().Add("content-type", "application/json")
_, _ = rw.Write(introspectJSON)
})
}
testserver := httptest.NewServer(mux)
u, _ := url.Parse(testserver.URL)
fmt.Println("The URL was: " + u.String())
return u, testserver
}
func newTestOIDCSetup(redeemJSON []byte, profileJSON []byte, introspectJSON []byte) (*httptest.Server, *OIDCProvider) {
_, server := newOIDCServer(redeemJSON, profileJSON, introspectJSON)
serverURL, _ := url.Parse(server.URL)
provider := newOIDCProvider(serverURL, false)
return server, provider
}
func TestOIDCProviderGetLoginURL(t *testing.T) {
serverURL := &url.URL{
Scheme: "https",
Host: "oauth2proxy.oidctest",
}
provider := newOIDCProvider(serverURL, true)
n, err := encryption.Nonce(32)
assert.NoError(t, err)
nonce := base64.RawURLEncoding.EncodeToString(n)
// SkipNonce defaults to true
skipNonce := provider.GetLoginURL("http://redirect/", "", nonce, url.Values{})
assert.NotContains(t, skipNonce, "nonce")
provider.SkipNonce = false
withNonce := provider.GetLoginURL("http://redirect/", "", nonce, url.Values{})
assert.Contains(t, withNonce, fmt.Sprintf("nonce=%s", nonce))
assert.NotContains(t, withNonce, "code_challenge")
assert.NotContains(t, withNonce, "code_challenge_method")
}
func TestOIDCProviderRedeem(t *testing.T) {
idToken, _ := newSignedTestIDToken(defaultIDToken)
body, _ := json.Marshal(redeemTokenResponse{
AccessToken: accessToken,
ExpiresIn: 10,
TokenType: "Bearer",
RefreshToken: refreshToken,
IDToken: idToken,
})
server, provider := newTestOIDCSetup(body, []byte(`{}`), []byte(`{}`))
defer server.Close()
session, err := provider.Redeem(context.Background(), provider.RedeemURL.String(), "code1234", "")
assert.Equal(t, nil, err)
assert.Equal(t, defaultIDToken.Email, session.Email)
assert.Equal(t, accessToken, session.AccessToken)
assert.Equal(t, idToken, session.IDToken)
assert.Equal(t, refreshToken, session.RefreshToken)
assert.Equal(t, "123456789", session.User)
}
func TestOIDCProviderRedeem_custom_userid(t *testing.T) {
idToken, _ := newSignedTestIDToken(defaultIDToken)
body, _ := json.Marshal(redeemTokenResponse{
AccessToken: accessToken,
ExpiresIn: 10,
TokenType: "Bearer",
RefreshToken: refreshToken,
IDToken: idToken,
})
server, provider := newTestOIDCSetup(body, []byte(`{}`), []byte(`{}`))
provider.EmailClaim = "phone_number"
defer server.Close()
session, err := provider.Redeem(context.Background(), provider.RedeemURL.String(), "code1234", "")
assert.Equal(t, nil, err)
assert.Equal(t, defaultIDToken.Phone, session.Email)
}
func TestOIDCProviderRefreshSessionIfNeededWithoutIdToken(t *testing.T) {
idToken, _ := newSignedTestIDToken(defaultIDToken)
body, _ := json.Marshal(redeemTokenResponse{
AccessToken: accessToken,
ExpiresIn: 10,
TokenType: "Bearer",
RefreshToken: refreshToken,
})
server, provider := newTestOIDCSetup(body, []byte(`{}`), []byte(`{}`))
defer server.Close()
existingSession := &sessions.SessionState{
AccessToken: "changeit",
IDToken: idToken,
CreatedAt: nil,
ExpiresOn: nil,
RefreshToken: refreshToken,
Email: "janedoe@example.com",
User: "11223344",
}
refreshed, err := provider.RefreshSession(context.Background(), existingSession)
assert.Equal(t, nil, err)
assert.Equal(t, refreshed, true)
assert.Equal(t, "janedoe@example.com", existingSession.Email)
assert.Equal(t, accessToken, existingSession.AccessToken)
assert.Equal(t, idToken, existingSession.IDToken)
assert.Equal(t, refreshToken, existingSession.RefreshToken)
assert.Equal(t, "11223344", existingSession.User)
}
func TestOIDCProviderRefreshSessionIfNeededWithIdToken(t *testing.T) {
idToken, _ := newSignedTestIDToken(defaultIDToken)
body, _ := json.Marshal(redeemTokenResponse{
AccessToken: accessToken,
ExpiresIn: 10,
TokenType: "Bearer",
RefreshToken: refreshToken,
IDToken: idToken,
})
server, provider := newTestOIDCSetup(body, []byte(`{}`), []byte(`{}`))
defer server.Close()
existingSession := &sessions.SessionState{
AccessToken: "changeit",
IDToken: "changeit",
CreatedAt: nil,
ExpiresOn: nil,
RefreshToken: refreshToken,
Email: "changeit",
User: "changeit",
}
refreshed, err := provider.RefreshSession(context.Background(), existingSession)
assert.Equal(t, nil, err)
assert.Equal(t, refreshed, true)
assert.Equal(t, defaultIDToken.Email, existingSession.Email)
assert.Equal(t, defaultIDToken.Subject, existingSession.User)
assert.Equal(t, accessToken, existingSession.AccessToken)
assert.Equal(t, idToken, existingSession.IDToken)
assert.Equal(t, refreshToken, existingSession.RefreshToken)
}
func TestOIDCProviderCreateSessionFromToken(t *testing.T) {
testCases := map[string]struct {
IDToken idTokenClaims
GroupsClaim string
ExpectedUser string
ExpectedEmail string
ExpectedGroups []string
}{
"Default IDToken": {
IDToken: defaultIDToken,
GroupsClaim: "groups",
ExpectedUser: "123456789",
ExpectedEmail: "janed@me.com",
ExpectedGroups: []string{"test:a", "test:b"},
},
"Minimal IDToken with no email claim": {
IDToken: minimalIDToken,
GroupsClaim: "groups",
ExpectedUser: "123456789",
ExpectedEmail: "123456789",
ExpectedGroups: nil,
},
"Custom Groups Claim": {
IDToken: defaultIDToken,
GroupsClaim: "roles",
ExpectedUser: "123456789",
ExpectedEmail: "janed@me.com",
ExpectedGroups: []string{"test:c", "test:d"},
},
"Complex Groups Claim": {
IDToken: complexGroupsIDToken,
GroupsClaim: "groups",
ExpectedUser: "123456789",
ExpectedEmail: "complex@claims.com",
ExpectedGroups: []string{
"{\"groupId\":\"Admin Group Id\",\"roles\":[\"Admin\"]}",
"12345",
"Just::A::String",
},
},
}
for testName, tc := range testCases {
t.Run(testName, func(t *testing.T) {
server, provider := newTestOIDCSetup([]byte(`{}`), []byte(`{}`), []byte(`{}`))
provider.GroupsClaim = tc.GroupsClaim
defer server.Close()
rawIDToken, err := newSignedTestIDToken(tc.IDToken)
assert.NoError(t, err)
ss, err := provider.CreateSessionFromToken(context.Background(), rawIDToken)
assert.NoError(t, err)
assert.Equal(t, tc.ExpectedUser, ss.User)
assert.Equal(t, tc.ExpectedEmail, ss.Email)
assert.Equal(t, tc.ExpectedGroups, ss.Groups)
assert.Equal(t, rawIDToken, ss.IDToken)
assert.Equal(t, rawIDToken, ss.AccessToken)
assert.Equal(t, "", ss.RefreshToken)
})
}
}
// Vozniak
// func TestOIDCProvider_EnrichSession(t *testing.T) {
// testCases := map[string]struct {
// ExistingSession *sessions.SessionState
// EmailClaim string
// GroupsClaim string
// ProfileJSON map[string]interface{}
// IntrospectJSON map[string]interface{}
// ExpectedError error
// ExpectedSession *sessions.SessionState
// }{
// "Already Populated": {
// ExistingSession: &sessions.SessionState{
// User: "already",
// Email: "already@populated.com",
// Groups: []string{"already", "populated"},
// IDToken: idToken,
// AccessToken: accessToken,
// RefreshToken: refreshToken,
// },
// EmailClaim: "email",
// GroupsClaim: "groups",
// ProfileJSON: map[string]interface{}{
// "email": "new@thing.com",
// "groups": []string{"new", "thing"},
// },
// IntrospectJSON: map[string]interface{}{
// "active": true,
// },
// ExpectedError: nil,
// ExpectedSession: &sessions.SessionState{
// User: "already",
// Email: "already@populated.com",
// Groups: []string{"already", "populated"},
// IDToken: idToken,
// AccessToken: accessToken,
// RefreshToken: refreshToken,
// IntrospectClaims: "eyJhY3RpdmUiOnRydWV9",
// },
// },
// "Missing Email": {
// ExistingSession: &sessions.SessionState{
// User: "missing.email",
// Groups: []string{"already", "populated"},
// IDToken: idToken,
// AccessToken: accessToken,
// RefreshToken: refreshToken,
// },
// EmailClaim: "email",
// GroupsClaim: "groups",
// ProfileJSON: map[string]interface{}{
// "email": "found@email.com",
// "groups": []string{"new", "thing"},
// },
// IntrospectJSON: map[string]interface{}{
// "active": true,
// },
// ExpectedError: nil,
// ExpectedSession: &sessions.SessionState{
// User: "missing.email",
// Email: "found@email.com",
// Groups: []string{"already", "populated"},
// IDToken: idToken,
// AccessToken: accessToken,
// RefreshToken: refreshToken,
// IntrospectClaims: "eyJhY3RpdmUiOnRydWV9",
// },
// },
// "Missing Email Only in Profile URL": {
// ExistingSession: &sessions.SessionState{
// User: "missing.email",
// IDToken: idToken,
// AccessToken: accessToken,
// RefreshToken: refreshToken,
// },
// EmailClaim: "email",
// GroupsClaim: "groups",
// ProfileJSON: map[string]interface{}{
// "email": "found@email.com",
// },
// IntrospectJSON: map[string]interface{}{
// "active": true,
// },
// ExpectedError: nil,
// ExpectedSession: &sessions.SessionState{
// User: "missing.email",
// Email: "found@email.com",
// IDToken: idToken,
// AccessToken: accessToken,
// RefreshToken: refreshToken,
// IntrospectClaims: "eyJhY3RpdmUiOnRydWV9",
// },
// },
// "Missing Email with Custom Claim": {
// ExistingSession: &sessions.SessionState{
// User: "missing.email",
// Groups: []string{"already", "populated"},
// IDToken: idToken,
// AccessToken: accessToken,
// RefreshToken: refreshToken,
// },
// EmailClaim: "weird",
// GroupsClaim: "groups",
// ProfileJSON: map[string]interface{}{
// "weird": "weird@claim.com",
// "groups": []string{"new", "thing"},
// },
// IntrospectJSON: map[string]interface{}{
// "active": true,
// },
// ExpectedError: nil,
// ExpectedSession: &sessions.SessionState{
// User: "missing.email",
// Email: "weird@claim.com",
// Groups: []string{"already", "populated"},
// IDToken: idToken,
// AccessToken: accessToken,
// RefreshToken: refreshToken,
// IntrospectClaims: "eyJhY3RpdmUiOnRydWV9",
// },
// },
// "Missing Email not in Profile URL": {
// ExistingSession: &sessions.SessionState{
// User: "missing.email",
// Groups: []string{"already", "populated"},
// IDToken: idToken,
// AccessToken: accessToken,
// RefreshToken: refreshToken,
// },
// EmailClaim: "email",
// GroupsClaim: "groups",
// ProfileJSON: map[string]interface{}{
// "groups": []string{"new", "thing"},
// },
// IntrospectJSON: map[string]interface{}{
// "active": true,
// },
// ExpectedError: errors.New("neither the id_token nor the profileURL set an email"),
// ExpectedSession: &sessions.SessionState{
// User: "missing.email",
// Groups: []string{"already", "populated"},
// IDToken: idToken,
// AccessToken: accessToken,
// RefreshToken: refreshToken,
// IntrospectClaims: "eyJhY3RpdmUiOnRydWV9",
// },
// },
// "Missing Groups": {
// ExistingSession: &sessions.SessionState{
// User: "already",
// Email: "already@populated.com",
// Groups: nil,
// IDToken: idToken,
// AccessToken: accessToken,
// RefreshToken: refreshToken,
// },
// EmailClaim: "email",
// GroupsClaim: "groups",
// ProfileJSON: map[string]interface{}{
// "email": "new@thing.com",
// "groups": []string{"new", "thing"},
// },
// IntrospectJSON: map[string]interface{}{
// "active": true,
// },
// ExpectedError: nil,
// ExpectedSession: &sessions.SessionState{
// User: "already",
// Email: "already@populated.com",
// Groups: []string{"new", "thing"},
// IDToken: idToken,
// AccessToken: accessToken,
// RefreshToken: refreshToken,
// IntrospectClaims: "eyJhY3RpdmUiOnRydWV9",
// },
// },
// "Missing Groups with Complex Groups in Profile URL": {
// ExistingSession: &sessions.SessionState{
// User: "already",
// Email: "already@populated.com",
// Groups: nil,
// IDToken: idToken,
// AccessToken: accessToken,
// RefreshToken: refreshToken,
// },
// EmailClaim: "email",
// GroupsClaim: "groups",
// ProfileJSON: map[string]interface{}{
// "email": "new@thing.com",
// "groups": []map[string]interface{}{
// {
// "groupId": "Admin Group Id",
// "roles": []string{"Admin"},
// },
// },
// },
// IntrospectJSON: map[string]interface{}{
// "active": true,
// },
// ExpectedError: nil,
// ExpectedSession: &sessions.SessionState{
// User: "already",
// Email: "already@populated.com",
// Groups: []string{"{\"groupId\":\"Admin Group Id\",\"roles\":[\"Admin\"]}"},
// IDToken: idToken,
// AccessToken: accessToken,
// RefreshToken: refreshToken,
// IntrospectClaims: "eyJhY3RpdmUiOnRydWV9",
// },
// },
// "Missing Groups with Singleton Complex Group in Profile URL": {
// ExistingSession: &sessions.SessionState{
// User: "already",
// Email: "already@populated.com",
// Groups: nil,
// IDToken: idToken,
// AccessToken: accessToken,
// RefreshToken: refreshToken,
// },
// EmailClaim: "email",
// GroupsClaim: "groups",
// ProfileJSON: map[string]interface{}{
// "email": "new@thing.com",
// "groups": map[string]interface{}{
// "groupId": "Admin Group Id",
// "roles": []string{"Admin"},
// },
// },
// IntrospectJSON: map[string]interface{}{
// "active": true,
// },
// ExpectedError: nil,
// ExpectedSession: &sessions.SessionState{
// User: "already",
// Email: "already@populated.com",
// Groups: []string{"{\"groupId\":\"Admin Group Id\",\"roles\":[\"Admin\"]}"},
// IDToken: idToken,
// AccessToken: accessToken,
// RefreshToken: refreshToken,
// IntrospectClaims: "eyJhY3RpdmUiOnRydWV9",
// },
// },
// "Empty Groups Claims": {
// ExistingSession: &sessions.SessionState{
// User: "already",
// Email: "already@populated.com",
// Groups: []string{},
// IDToken: idToken,
// AccessToken: accessToken,
// RefreshToken: refreshToken,
// },
// EmailClaim: "email",
// GroupsClaim: "groups",
// ProfileJSON: map[string]interface{}{
// "email": "new@thing.com",
// "groups": []string{"new", "thing"},
// },
// IntrospectJSON: map[string]interface{}{
// "active": true,
// },
// ExpectedError: nil,
// ExpectedSession: &sessions.SessionState{
// User: "already",
// Email: "already@populated.com",
// Groups: []string{},
// IDToken: idToken,
// AccessToken: accessToken,
// RefreshToken: refreshToken,
// IntrospectClaims: "eyJhY3RpdmUiOnRydWV9",
// },
// },
// "Missing Groups with Custom Claim": {
// ExistingSession: &sessions.SessionState{
// User: "already",
// Email: "already@populated.com",
// Groups: nil,
// IDToken: idToken,
// AccessToken: accessToken,
// RefreshToken: refreshToken,
// },
// EmailClaim: "email",
// GroupsClaim: "roles",
// ProfileJSON: map[string]interface{}{
// "email": "new@thing.com",
// "roles": []string{"new", "thing", "roles"},
// },
// IntrospectJSON: map[string]interface{}{
// "active": true,
// },
// ExpectedError: nil,
// ExpectedSession: &sessions.SessionState{
// User: "already",
// Email: "already@populated.com",
// Groups: []string{"new", "thing", "roles"},
// IDToken: idToken,
// AccessToken: accessToken,
// RefreshToken: refreshToken,
// IntrospectClaims: "eyJhY3RpdmUiOnRydWV9",
// },
// },
// "Missing Groups String Profile URL Response": {
// ExistingSession: &sessions.SessionState{
// User: "already",
// Email: "already@populated.com",
// Groups: nil,
// IDToken: idToken,
// AccessToken: accessToken,
// RefreshToken: refreshToken,
// },
// EmailClaim: "email",
// GroupsClaim: "groups",
// ProfileJSON: map[string]interface{}{
// "email": "new@thing.com",
// "groups": "singleton",
// },
// IntrospectJSON: map[string]interface{}{
// "active": true,
// },
// ExpectedError: nil,
// ExpectedSession: &sessions.SessionState{
// User: "already",
// Email: "already@populated.com",
// Groups: []string{"singleton"},
// IDToken: idToken,
// AccessToken: accessToken,
// RefreshToken: refreshToken,
// IntrospectClaims: "eyJhY3RpdmUiOnRydWV9",
// },
// },
// "Missing Groups in both Claims and Profile URL": {
// ExistingSession: &sessions.SessionState{
// User: "already",
// Email: "already@populated.com",
// IDToken: idToken,
// AccessToken: accessToken,
// RefreshToken: refreshToken,
// },
// EmailClaim: "email",
// GroupsClaim: "groups",
// ProfileJSON: map[string]interface{}{
// "email": "new@thing.com",
// },
// IntrospectJSON: map[string]interface{}{
// "active": true,
// },
// ExpectedError: nil,
// ExpectedSession: &sessions.SessionState{
// User: "already",
// Email: "already@populated.com",
// IDToken: idToken,
// AccessToken: accessToken,
// RefreshToken: refreshToken,
// IntrospectClaims: "eyJhY3RpdmUiOnRydWV9",
// },
// },
// "Introspection Response added in session": {
// ExistingSession: &sessions.SessionState{
// User: "already",
// Email: "already@populated.com",
// IDToken: idToken,
// AccessToken: accessToken,
// RefreshToken: refreshToken,
// },
// EmailClaim: "email",
// GroupsClaim: "groups",
// ProfileJSON: map[string]interface{}{
// "email": "new@thing.com",
// },
// IntrospectJSON: map[string]interface{}{
// "active": true,
// "exp": 1613553280,
// "iat": 1613549680,
// "sub": "00u65ah10oSUDoNd65d6",
// "aud": "0oa5t6ts1XZgPOOSA5d6",
// "iss": "https://dev.authprovider.com/oauth2/default",
// "jti": "ID.ShxppK0vysORHsr8S9GSPPRV-Dy1PFhqx3fPYwtijc0",
// "token_type": "Bearer",
// "at_hash": "_1EsbE1ZpyMOLiu-VdBTlg",
// "idp": "0oa5t8x90SALHBBk85d6",
// "auth_time": 1613549026,
// "amr": []string{"pwd"},
// },
// ExpectedError: nil,
// ExpectedSession: &sessions.SessionState{
// User: "already",
// Email: "already@populated.com",
// IDToken: idToken,
// AccessToken: accessToken,
// RefreshToken: refreshToken,
// IntrospectClaims: "eyJhY3RpdmUiOnRydWUsImFtciI6WyJwd2QiXSwiYXRfaGFzaCI6Il8xRXNiRTFacHlNT0xpdS1WZEJUbGciLCJhdWQiOiIwb2E1dDZ0czFYWmdQT09TQTVkNiIsImF1dGhfdGltZSI6MTYxMzU0OTAyNiwiZXhwIjoxNjEzNTUzMjgwLCJpYXQiOjE2MTM1NDk2ODAsImlkcCI6IjBvYTV0OHg5MFNBTEhCQms4NWQ2IiwiaXNzIjoiaHR0cHM6Ly9kZXYuYXV0aHByb3ZpZGVyLmNvbS9vYXV0aDIvZGVmYXVsdCIsImp0aSI6IklELlNoeHBwSzB2eXNPUkhzcjhTOUdTUFBSVi1EeTFQRmhxeDNmUFl3dGlqYzAiLCJzdWIiOiIwMHU2NWFoMTBvU1VEb05kNjVkNiIsInRva2VuX3R5cGUiOiJCZWFyZXIifQ==",
// },
// },
// }
// for testName, tc := range testCases {
// t.Run(testName, func(t *testing.T) {
// profileJSON, err := json.Marshal(tc.ProfileJSON)
// assert.NoError(t, err)
// introspectJSON, err := json.Marshal(tc.IntrospectJSON)
// assert.NoError(t, err)
// server, provider := newTestOIDCSetup([]byte(`{}`), profileJSON, introspectJSON)
// assert.NoError(t, err)
// provider.EmailClaim = tc.EmailClaim
// provider.GroupsClaim = tc.GroupsClaim
// defer server.Close()
// err = provider.EnrichSession(context.Background(), tc.ExistingSession)
// assert.Equal(t, tc.ExpectedError, err)
// assert.Equal(t, *tc.ExpectedSession, *tc.ExistingSession)
// })
// }
// }