oidc: refuse logins whose subject the provider identifier would alter

This commit is contained in:
Kristoffer Dalby committed 2026-10-30 14:55:37 +00:00
1 parent b22dd41b51
commit ffc668d48e
4 files changed
+74

No files matched your search

+7
View File
@@ -370,6 +370,13 @@ func (a *AuthProviderOIDC) OIDCCallbackHandler(
return
}
if !claims.IdentifierKeepsSubject() {
log.Warn().Str("subject", claims.Sub).Msg("OIDC subject would be altered by provider identifier cleaning")
httpUserError(writer, NewHTTPError(http.StatusForbidden, "OIDC subject cannot be used as a provider identifier", nil))
return
}
user, err := a.createOrUpdateUserFromClaim(&claims)
if err != nil {
httpUserError(writer, NewHTTPError(
+21
View File
@@ -1449,6 +1449,27 @@ func TestMergeEmailClaims(t *testing.T) {
}
}
func TestOIDCRefusesAlteredSubject(t *testing.T) {
b := newOIDCBrowser(t)
b.idp.QueueUser(&mockoidc.MockUser{Subject: "alice", PreferredUsername: "alice"})
_, registerURL := b.pendingNode(t)
status, _, _ := b.get(t, registerURL)
require.Equal(t, http.StatusOK, status)
before, err := b.app.state.GetUserByOIDCIdentifier(b.idp.Issuer() + "/alice")
require.NoError(t, err)
b.idp.QueueUser(&mockoidc.MockUser{Subject: "alice/", PreferredUsername: "renamed"})
_, registerURL = b.pendingNode(t)
status, _, _ = b.get(t, registerURL)
assert.Equal(t, http.StatusForbidden, status)
users, err := b.app.state.ListAllUsers()
require.NoError(t, err)
require.Len(t, users, 1)
assert.Equal(t, *before, users[0], "a refused subject must not update an existing row")
}
func TestOIDCRegistrationRejectsOversizedMergedHostinfo(t *testing.T) {
t.Parallel()
+7
View File
@@ -354,6 +354,13 @@ func (c *OIDCClaims) Identifier() string {
return CleanIdentifier(result)
}
// IdentifierKeepsSubject reports whether the stored key preserves the subject.
// Refusing altered subjects avoids changing existing provider identifiers.
func (c *OIDCClaims) IdentifierKeepsSubject() bool {
return c.Identifier() == CleanIdentifier(c.Iss)+"/"+
(&url.URL{Path: c.Sub, RawPath: c.Sub}).EscapedPath()
}
// CleanIdentifier cleans a potentially malformed identifier by removing double slashes
// while preserving protocol specifications like http://. This function will:
// - Trim all whitespace from the beginning and end of the identifier
+39
View File
@@ -552,3 +552,42 @@ func TestOIDCClaimsJSONToUser(t *testing.T) {
})
}
}
func TestOIDCClaimsIdentifierKeepsSubject(t *testing.T) {
kept := []string{
"c8872e9e-9a4f-4b8f-b111-c6191595d450",
"1234567890", "0123456789abcdef", "ab_C-dEF0123",
"ab/cd+ef==", "auth0|12345", "google-oauth2|12345",
"samlp|connection|alice@example.com", "f:realm:alice",
"alice@example.com", "\u7528\u6237", "alice smith", ".", "..",
}
refused := []string{
"", "/alice", "alice/", "alice//smith", " alice", "alice ",
"alice/ smith", "alice /smith", "alice#", "alice?x", "alice%23",
"alice%3Fx", "alice%2F", "alice%252F", "alice%20", "alice%2520",
"alice%zz", "a\x01b", "a\nb", "a\tb",
}
for _, issuer := range []string{
"https://sso.example.com",
"https://sso.example.com/realm/",
"https://login.microsoftonline.com/tenant/v2.0",
} {
t.Run(issuer, func(t *testing.T) {
identifiers := make(map[string]string)
for _, subject := range kept {
claims := OIDCClaims{Iss: issuer, Sub: subject}
assert.True(t, claims.IdentifierKeepsSubject(), "subject %q", subject)
identifier := claims.Identifier()
assert.NotContains(t, identifiers, identifier, "subject %q", subject)
identifiers[identifier] = subject
}
for _, subject := range refused {
claims := OIDCClaims{Iss: issuer, Sub: subject}
assert.False(t, claims.IdentifierKeepsSubject(), "subject %q", subject)
}
})
}
}