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) }