Files
coder/coderd/coderdtest/oidctest/idp_test.go
Steven Masley 4f01372179 feat: implement disabling oidc issuer checks (#13991)
* use DANGEROUS prefix and drop a warning log
2024-07-24 16:45:47 -05:00

152 lines
4.6 KiB
Go

package oidctest_test
import (
"context"
"crypto"
"net/http"
"testing"
"time"
"github.com/golang-jwt/jwt/v4"
"github.com/stretchr/testify/assert"
"golang.org/x/xerrors"
"github.com/coreos/go-oidc/v3/oidc"
"github.com/stretchr/testify/require"
"golang.org/x/oauth2"
"github.com/coder/coder/v2/coderd"
"github.com/coder/coder/v2/coderd/coderdtest/oidctest"
"github.com/coder/coder/v2/testutil"
)
// TestFakeIDPBasicFlow tests the basic flow of the fake IDP.
// It is done all in memory with no actual network requests.
// nolint:bodyclose
func TestFakeIDPBasicFlow(t *testing.T) {
t.Parallel()
fake := oidctest.NewFakeIDP(t,
oidctest.WithLogging(t, nil),
)
cfg := fake.OIDCConfig(t, nil)
cli := fake.HTTPClient(nil)
ctx := oidc.ClientContext(context.Background(), cli)
const expectedState = "random-state"
var token *oauth2.Token
// This is the Coder callback using an actual network request.
fake.SetCoderdCallbackHandler(func(w http.ResponseWriter, r *http.Request) {
// Emulate OIDC flow
code := r.URL.Query().Get("code")
state := r.URL.Query().Get("state")
assert.Equal(t, expectedState, state, "state mismatch")
oauthToken, err := cfg.Exchange(ctx, code)
if assert.NoError(t, err, "failed to exchange code") {
assert.NotEmpty(t, oauthToken.AccessToken, "access token is empty")
assert.NotEmpty(t, oauthToken.RefreshToken, "refresh token is empty")
}
token = oauthToken
})
//nolint:bodyclose
resp := fake.OIDCCallback(t, expectedState, jwt.MapClaims{})
require.Equal(t, http.StatusOK, resp.StatusCode)
// Test the user info
_, err := cfg.Provider.UserInfo(ctx, oauth2.StaticTokenSource(token))
require.NoError(t, err)
// Now test it can refresh
refreshed, err := cfg.TokenSource(ctx, &oauth2.Token{
AccessToken: token.AccessToken,
RefreshToken: token.RefreshToken,
Expiry: time.Now().Add(time.Minute * -1),
}).Token()
require.NoError(t, err, "failed to refresh token")
require.NotEmpty(t, refreshed.AccessToken, "access token is empty on refresh")
}
// TestIDPIssuerMismatch emulates a situation where the IDP issuer url does
// not match the one in the well-known config and claims.
// This can happen in some edge cases and in some azure configurations.
//
// This test just makes sure a fake IDP can set up this scenario.
func TestIDPIssuerMismatch(t *testing.T) {
t.Parallel()
const proxyURL = "https://proxy.com"
const primaryURL = "https://primary.com"
fake := oidctest.NewFakeIDP(t,
oidctest.WithIssuer(proxyURL),
oidctest.WithDefaultIDClaims(jwt.MapClaims{
"iss": primaryURL,
}),
oidctest.WithHookWellKnown(func(r *http.Request, j *oidctest.ProviderJSON) error {
// host should be proxy.com, but we return the primaryURL
if r.Host != "proxy.com" {
return xerrors.Errorf("unexpected host: %s", r.Host)
}
j.Issuer = primaryURL
return nil
}),
oidctest.WithLogging(t, nil),
)
ctx := testutil.Context(t, testutil.WaitMedium)
// Do not use real network requests
cli := fake.HTTPClient(nil)
ctx = oidc.ClientContext(ctx, cli)
// Allow the issuer mismatch
verifierContext := oidc.InsecureIssuerURLContext(ctx, "this field does not matter")
p, err := oidc.NewProvider(verifierContext, "https://proxy.com")
require.NoError(t, err, "failed to create OIDC provider")
oauthConfig := fake.OauthConfig(t, nil)
cfg := &coderd.OIDCConfig{
OAuth2Config: oauthConfig,
Provider: p,
Verifier: oidc.NewVerifier(fake.WellknownConfig().Issuer, &oidc.StaticKeySet{
PublicKeys: []crypto.PublicKey{fake.PublicKey()},
}, &oidc.Config{
SkipIssuerCheck: true,
ClientID: oauthConfig.ClientID,
SupportedSigningAlgs: []string{
"RS256",
},
}),
UsernameField: "preferred_username",
EmailField: "email",
AuthURLParams: map[string]string{"access_type": "offline"},
}
const expectedState = "random-state"
var token *oauth2.Token
fake.SetCoderdCallbackHandler(func(w http.ResponseWriter, r *http.Request) {
// Emulate OIDC flow
code := r.URL.Query().Get("code")
state := r.URL.Query().Get("state")
assert.Equal(t, expectedState, state, "state mismatch")
oauthToken, err := cfg.Exchange(ctx, code)
if assert.NoError(t, err, "failed to exchange code") {
assert.NotEmpty(t, oauthToken.AccessToken, "access token is empty")
assert.NotEmpty(t, oauthToken.RefreshToken, "refresh token is empty")
}
token = oauthToken
})
//nolint:bodyclose
resp := fake.OIDCCallback(t, expectedState, nil) // Use default claims
require.Equal(t, http.StatusOK, resp.StatusCode)
idToken, err := cfg.Verifier.Verify(ctx, token.Extra("id_token").(string))
require.NoError(t, err)
require.Equal(t, primaryURL, idToken.Issuer)
}