Skip to content

Commit a89f046

Browse files
committed
oidc: add API for determining when issuer URLs mismatch
Fixes #471 Fixes #481
1 parent 6a69b6d commit a89f046

2 files changed

Lines changed: 72 additions & 1 deletion

File tree

oidc/oidc.go

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -244,10 +244,34 @@ func (p *ProviderConfig) NewProvider(ctx context.Context) *Provider {
244244
}
245245
}
246246

247+
// IssuerMismatchError is returned by [NewProvider] when the "iss" value
248+
// reported by the upstream is different than the expected value.
249+
//
250+
// Issuer mismatches can occur due to trailing slashes ("https://example.com"
251+
// vs. "https://example.com/") or represent significant misconfiguration for
252+
// multi-tenant issuers.
253+
//
254+
// Issuers must match exactly as they are also used to validate ID Tokens.
255+
//
256+
// https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderMetadata
257+
type IssuerMismatchError struct {
258+
// The value provided to this package. The expected value.
259+
Provided string
260+
// The value advertised by the discovery document.
261+
Discovered string
262+
}
263+
264+
func (e *IssuerMismatchError) Error() string {
265+
return fmt.Sprintf("oidc: issuer URL provided to client (%q) did not match the issuer URL returned by provider (%q)", e.Provided, e.Discovered)
266+
}
267+
247268
// NewProvider uses the OpenID Connect discovery mechanism to construct a Provider.
248269
// The issuer is the URL identifier for the service. For example: "https://accounts.google.com"
249270
// or "https://login.salesforce.com".
250271
//
272+
// If the "iss" value returned in the discovery document doesn't match the value
273+
// provided here, [IssuerMismatchError] is returned.
274+
//
251275
// OpenID Connect providers that don't implement discovery or host the discovery
252276
// document at a non-spec compliant path (such as requiring a URL parameter),
253277
// should use [ProviderConfig] instead.
@@ -285,7 +309,10 @@ func NewProvider(ctx context.Context, issuer string) (*Provider, error) {
285309
issuerURL = issuer
286310
}
287311
if p.Issuer != issuerURL && !skipIssuerValidation {
288-
return nil, fmt.Errorf("oidc: issuer URL provided to client (%q) did not match the issuer URL returned by provider (%q)", issuer, p.Issuer)
312+
return nil, &IssuerMismatchError{
313+
Provided: issuerURL,
314+
Discovered: p.Issuer,
315+
}
289316
}
290317
var algs []string
291318
for _, a := range p.Algorithms {

oidc/oidc_test.go

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"crypto/elliptic"
77
"crypto/rand"
88
"encoding/json"
9+
"errors"
910
"fmt"
1011
"io"
1112
"net/http"
@@ -343,6 +344,49 @@ func TestNewProvider(t *testing.T) {
343344
}
344345
}
345346

347+
func TestIssuerMismatchError(t *testing.T) {
348+
ctx := t.Context()
349+
350+
var issuer string
351+
hf := func(w http.ResponseWriter, r *http.Request) {
352+
if r.URL.Path != "/.well-known/openid-configuration" {
353+
http.NotFound(w, r)
354+
return
355+
}
356+
w.Header().Set("Content-Type", "application/json")
357+
// The discovery document advertises an issuer that differs from the
358+
// one requested (note the trailing slash), which must be rejected.
359+
io.WriteString(w, `{
360+
"issuer": "`+issuer+`/",
361+
"authorization_endpoint": "https://example.com/auth",
362+
"token_endpoint": "https://example.com/token",
363+
"jwks_uri": "https://example.com/keys",
364+
"id_token_signing_alg_values_supported": ["RS256"]
365+
}`)
366+
}
367+
s := httptest.NewServer(http.HandlerFunc(hf))
368+
defer s.Close()
369+
issuer = s.URL
370+
371+
_, err := NewProvider(ctx, issuer)
372+
if err == nil {
373+
t.Fatal("NewProvider(): expected error, got nil")
374+
}
375+
376+
var mismatchErr *IssuerMismatchError
377+
if !errors.As(err, &mismatchErr) {
378+
t.Fatalf("NewProvider() returned %T, want *IssuerMismatchError: %v", err, err)
379+
}
380+
381+
want := &IssuerMismatchError{
382+
Provided: issuer,
383+
Discovered: issuer + "/",
384+
}
385+
if !reflect.DeepEqual(mismatchErr, want) {
386+
t.Errorf("NewProvider() returned unexpected error, got=%+v, want=%+v", mismatchErr, want)
387+
}
388+
}
389+
346390
func TestProviderConfigJSON(t *testing.T) {
347391
// https://accounts.google.com/.well-known/openid-configuration
348392
testCase := `

0 commit comments

Comments
 (0)