Skip to content

feat(oidc): support for all in-spec attributes and scopes#777

Open
scottmckendry wants to merge 8 commits intotinyauthapp:mainfrom
scottmckendry:oidcc-user-attributes-scopes
Open

feat(oidc): support for all in-spec attributes and scopes#777
scottmckendry wants to merge 8 commits intotinyauthapp:mainfrom
scottmckendry:oidcc-user-attributes-scopes

Conversation

@scottmckendry
Copy link
Copy Markdown
Member

@scottmckendry scottmckendry commented Apr 9, 2026

A decent sized refactor that will require us to update the appropriate documentation if it gets merged. I'm happy to do this.

Should be backwards compatible (I think) with the old users config.

5 new test passes:

image

New scopes in authorization:

image

Now, I'm fine if we decide we're not interested in supporting phone number & address. That would make this PR a little bit smaller as a result. Selfishly, I just wanted to turn all of these tests green 🚀

Summary by CodeRabbit

Release Notes

  • New Features
    • Added support for phone and address OAuth scopes with visual indicators in the authorization flow
    • Extended OIDC userinfo claims to include phone number, address, and profile attributes (given name, family name, middle name, nickname, profile picture, website, gender, birthdate, timezone, locale)
    • Added configuration support for per-user identity attributes

@dosubot dosubot Bot added the size:XL This PR changes 500-999 lines, ignoring generated files. label Apr 9, 2026
@coderabbitai

This comment was marked as outdated.

@dosubot
Copy link
Copy Markdown

dosubot Bot commented Apr 9, 2026

Related Documentation

2 document(s) may need updating based on files changed in this PR:

Tinyauth's Space

configuration /tinyauth-docs/blob/main/src/content/docs/docs/reference/configuration.mdx — ⏳ Awaiting Merge
oidc /tinyauth-docs/blob/main/src/content/docs/docs/guides/oidc.mdx — ⏳ Awaiting Merge

Note: You must be authenticated to accept/decline updates.

How did I do? Any feedback?  Join Discord

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (2)
internal/service/oidc_service.go (1)

708-713: Silent failure on address unmarshal may hide data corruption.

If user.Address contains malformed JSON (not just "null"), the error is silently ignored and Address will be nil. Consider logging a warning for debugging purposes.

🔧 Optional: Log unmarshal errors for debugging
 	if slices.Contains(scopes, "address") {
 		var addr config.AddressClaim
-		if err := json.Unmarshal([]byte(user.Address), &addr); err == nil {
+		if err := json.Unmarshal([]byte(user.Address), &addr); err != nil {
+			tlog.App.Debug().Err(err).Str("sub", user.Sub).Msg("Failed to unmarshal address claim")
+		} else {
 			userInfo.Address = &addr
 		}
 	}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/service/oidc_service.go` around lines 708 - 713, The code silently
ignores json.Unmarshal errors when populating userInfo.Address from
user.Address; modify the block that checks slices.Contains(scopes, "address") so
that if json.Unmarshal(&addr) returns an error you log a warning containing the
error and the raw user.Address (and the relevant subject or user ID) instead of
dropping it silently, then only set userInfo.Address on success; reference the
same symbols (slices.Contains, user.Address, json.Unmarshal, addr,
userInfo.Address) and use the service's existing logger instance to emit the
warning.
internal/middleware/context_middleware.go (1)

116-119: Consider handling the case when GetLocalUser returns a user that doesn't exist.

The code assumes GetLocalUser always returns a valid user. If cookie.Username doesn't correspond to an existing local user (e.g., deleted after session creation), the code proceeds without error handling. The earlier SearchUser check (line 86-92) should catch this, but there's a potential TOCTOU gap if the user is deleted between the search and this call.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/middleware/context_middleware.go` around lines 116 - 119, The code
assumes m.auth.GetLocalUser(cookie.Username) always returns a valid user; update
the block where cookie.Provider == "local" to check the returned localUser for
nil (or a not-found indicator) before accessing localUser.Attributes, and handle
the missing-user case safely (e.g., set localAttributes to an empty map/slice,
skip attaching attributes, or return an auth error), and add a brief log entry
referencing cookie.Username so you can trace TOCTOU deletions; ensure this
change touches the GetLocalUser call site and the localAttributes assignment to
avoid a nil dereference.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@internal/config/config.go`:
- Around line 115-130: Implement a custom UnmarshalYAML on AuthConfig that
accepts both the new map format and the legacy list/string format for the Users
field: in UnmarshalYAML, unmarshal into a temporary struct mirroring AuthConfig
(to avoid recursion) and inspect the YAML node for Users; if Users is a sequence
of strings like "user:hash" (or sequence of single-key maps), convert those
entries into the map[string]UserConfig (setting Password field) before assigning
to the real AuthConfig.Users, otherwise accept the mapping form directly; after
normalizing the Users map, continue unmarshalling other fields and call
GetUsers() (or ensure existing GetUsers() will receive the normalized map).
Reference: implement UnmarshalYAML on type AuthConfig, handle field Users and
call GetUsers().

In `@internal/controller/user_controller.go`:
- Around line 157-165: In the branch that checks if userSearch.Type == "local",
replace the inconsistent lookup using req.Username when calling
controller.auth.GetLocalUser with the resolved username from userSearch (i.e.
use userSearch.Username) so both TOTP and non-TOTP flows use the same resolved
identifier; update any related sessionCookie population logic that assumes
values from localUser.Attributes (sessionCookie.Name, sessionCookie.Email) to
continue using the localUser returned for userSearch.Username to ensure
consistent behavior across branches.

In `@internal/controller/well_known_controller.go`:
- Line 62: ClaimsSupported currently lists only a subset of profile claims;
update the ClaimsSupported slice in well_known_controller.go (the
ClaimsSupported field) to include the missing profile claims: given_name,
family_name, middle_name, nickname, profile, picture, website, gender,
birthdate, zoneinfo, and locale so discovery documents accurately advertise all
implemented profile claims. Ensure the new claim names are added as strings in
the existing ClaimsSupported []string literal alongside the existing entries.

---

Nitpick comments:
In `@internal/middleware/context_middleware.go`:
- Around line 116-119: The code assumes m.auth.GetLocalUser(cookie.Username)
always returns a valid user; update the block where cookie.Provider == "local"
to check the returned localUser for nil (or a not-found indicator) before
accessing localUser.Attributes, and handle the missing-user case safely (e.g.,
set localAttributes to an empty map/slice, skip attaching attributes, or return
an auth error), and add a brief log entry referencing cookie.Username so you can
trace TOCTOU deletions; ensure this change touches the GetLocalUser call site
and the localAttributes assignment to avoid a nil dereference.

In `@internal/service/oidc_service.go`:
- Around line 708-713: The code silently ignores json.Unmarshal errors when
populating userInfo.Address from user.Address; modify the block that checks
slices.Contains(scopes, "address") so that if json.Unmarshal(&addr) returns an
error you log a warning containing the error and the raw user.Address (and the
relevant subject or user ID) instead of dropping it silently, then only set
userInfo.Address on success; reference the same symbols (slices.Contains,
user.Address, json.Unmarshal, addr, userInfo.Address) and use the service's
existing logger instance to emit the warning.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: fa259fe0-f366-4eeb-b986-b387e09ce3df

📥 Commits

Reviewing files that changed from the base of the PR and between 646e24d and 376b61c.

📒 Files selected for processing (15)
  • frontend/src/lib/i18n/locales/en.json
  • frontend/src/pages/authorize-page.tsx
  • internal/assets/migrations/000008_oidc_userinfo_profile.down.sql
  • internal/assets/migrations/000008_oidc_userinfo_profile.up.sql
  • internal/config/config.go
  • internal/controller/user_controller.go
  • internal/controller/well_known_controller.go
  • internal/middleware/context_middleware.go
  • internal/repository/models.go
  • internal/repository/oidc_queries.sql.go
  • internal/service/oidc_service.go
  • internal/utils/user_utils.go
  • internal/utils/user_utils_test.go
  • sql/oidc_queries.sql
  • sql/oidc_schemas.sql

Comment thread internal/config/config.go Outdated
Comment thread internal/controller/user_controller.go Outdated
Comment thread internal/controller/well_known_controller.go Outdated
@scottmckendry scottmckendry force-pushed the oidcc-user-attributes-scopes branch from 376b61c to 11e5158 Compare April 9, 2026 09:38
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
internal/service/oidc_service.go (1)

44-67: Consider removing unused ClaimSet fields.

After the generateIDToken change (lines 477-487), the profile-related fields in ClaimSet (GivenName, FamilyName, etc.) are never populated. Per the OIDC Core §5.4 comment, scope-requested claims now go in the userinfo response only.

These fields could be removed to avoid confusion, or retained if there's a future use case for returning claims in the ID token.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/service/oidc_service.go` around lines 44 - 67, The ClaimSet struct
contains many profile-related fields that are no longer populated by
generateIDToken; remove the unused fields (GivenName, FamilyName, MiddleName,
Nickname, Profile, Picture, Website, Gender, Birthdate, Zoneinfo, Locale,
PreferredUsername, Groups) from the ClaimSet type to avoid confusion, update any
code that referenced those fields (including serialization/tests), and keep
Nonce/Iat/Exp/Aud/Sub/Iss/Email/EmailVerified if still used by generateIDToken
and userinfo flows; ensure generateIDToken and any userinfo code still compile
and tests updated accordingly.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@internal/controller/user_controller.go`:
- Around line 108-112: The code references an undefined type utils.User; update
imports and the type to use the correct package: add an import for the config
package and change the localUser declaration/type from utils.User to config.User
(e.g., var localUser *config.User) and ensure the result of
controller.auth.GetLocalUser(userSearch.Username) is assigned to a config.User
value or pointer as appropriate so the types match (refer to localUser,
userSearch, and controller.auth.GetLocalUser to locate the change).

---

Nitpick comments:
In `@internal/service/oidc_service.go`:
- Around line 44-67: The ClaimSet struct contains many profile-related fields
that are no longer populated by generateIDToken; remove the unused fields
(GivenName, FamilyName, MiddleName, Nickname, Profile, Picture, Website, Gender,
Birthdate, Zoneinfo, Locale, PreferredUsername, Groups) from the ClaimSet type
to avoid confusion, update any code that referenced those fields (including
serialization/tests), and keep Nonce/Iat/Exp/Aud/Sub/Iss/Email/EmailVerified if
still used by generateIDToken and userinfo flows; ensure generateIDToken and any
userinfo code still compile and tests updated accordingly.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: de97d413-4c97-4e84-88b6-7183d64f2439

📥 Commits

Reviewing files that changed from the base of the PR and between 376b61c and 11e5158.

📒 Files selected for processing (16)
  • frontend/src/lib/i18n/locales/en.json
  • frontend/src/pages/authorize-page.tsx
  • internal/assets/migrations/000008_oidc_userinfo_profile.down.sql
  • internal/assets/migrations/000008_oidc_userinfo_profile.up.sql
  • internal/config/config.go
  • internal/controller/user_controller.go
  • internal/controller/well_known_controller.go
  • internal/controller/well_known_controller_test.go
  • internal/middleware/context_middleware.go
  • internal/repository/models.go
  • internal/repository/oidc_queries.sql.go
  • internal/service/oidc_service.go
  • internal/utils/user_utils.go
  • internal/utils/user_utils_test.go
  • sql/oidc_queries.sql
  • sql/oidc_schemas.sql
✅ Files skipped from review due to trivial changes (7)
  • frontend/src/pages/authorize-page.tsx
  • internal/controller/well_known_controller_test.go
  • internal/controller/well_known_controller.go
  • frontend/src/lib/i18n/locales/en.json
  • sql/oidc_schemas.sql
  • internal/assets/migrations/000008_oidc_userinfo_profile.up.sql
  • sql/oidc_queries.sql
🚧 Files skipped from review as they are similar to previous changes (3)
  • internal/assets/migrations/000008_oidc_userinfo_profile.down.sql
  • internal/middleware/context_middleware.go
  • internal/repository/models.go

Comment thread internal/controller/user_controller.go Outdated
@scottmckendry scottmckendry force-pushed the oidcc-user-attributes-scopes branch from 11e5158 to 0f58663 Compare April 9, 2026 09:48
@codecov
Copy link
Copy Markdown

codecov Bot commented Apr 9, 2026

Codecov Report

❌ Patch coverage is 39.83051% with 71 lines in your changes missing coverage. Please review.
✅ Project coverage is 21.11%. Comparing base (479f165) to head (8b1fd66).

Files with missing lines Patch % Lines
internal/repository/oidc_queries.sql.go 0.00% 39 Missing ⚠️
internal/service/oidc_service.go 54.05% 17 Missing ⚠️
internal/middleware/context_middleware.go 0.00% 14 Missing ⚠️
internal/bootstrap/app_bootstrap.go 0.00% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #777      +/-   ##
==========================================
+ Coverage   20.05%   21.11%   +1.06%     
==========================================
  Files          50       50              
  Lines        3995     4102     +107     
==========================================
+ Hits          801      866      +65     
- Misses       3121     3163      +42     
  Partials       73       73              

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@scottmckendry scottmckendry force-pushed the oidcc-user-attributes-scopes branch from 0f58663 to c2d630b Compare April 9, 2026 09:51
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
internal/middleware/context_middleware.go (1)

116-129: ⚠️ Potential issue | 🟠 Major

Propagate attribute-backed name and email in the local session path too.

This branch only fills UserContext.Attributes; UserContext.Name and UserContext.Email still come from the session cookie. StoreUserinfo later persists the top-level fields, so session-backed local logins can still publish stale/default name and email while basic-auth local logins use user.Attributes. Align this branch with the fallback logic added below.

Suggested fix
 			if cookie.Provider == "local" {
 				localUser := m.auth.GetLocalUser(cookie.Username)
 				localAttributes = localUser.Attributes
 			}
+
+			name := cookie.Name
+			if localAttributes.Name != "" {
+				name = localAttributes.Name
+			}
+
+			email := cookie.Email
+			if localAttributes.Email != "" {
+				email = localAttributes.Email
+			}
 
 			m.auth.RefreshSessionCookie(c)
 			c.Set("context", &config.UserContext{
 				Username:   cookie.Username,
-				Name:       cookie.Name,
-				Email:      cookie.Email,
+				Name:       name,
+				Email:      email,
 				Provider:   cookie.Provider,
 				IsLoggedIn: true,
 				LdapGroups: strings.Join(ldapGroups, ","),
 				Attributes: localAttributes,
 			})
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/middleware/context_middleware.go` around lines 116 - 129, When
cookie.Provider == "local" after calling localUser :=
m.auth.GetLocalUser(cookie.Username) and setting localAttributes, also prefer
attribute-backed name/email: if localUser.Attributes contains "name" or "email"
use those values to set UserContext.Name and UserContext.Email instead of the
cookie values; then call m.auth.RefreshSessionCookie(c) and create the context
with Username, Name, Email, Provider, IsLoggedIn, LdapGroups and Attributes (so
the c.Set("context", &config.UserContext{...}) block uses the attribute-backed
Name/Email when present).
🧹 Nitpick comments (1)
internal/utils/user_utils_test.go (1)

147-151: Consider adding a test case for empty username.

The implementation in ParseUsersFromMap validates empty usernames (after trimming), but there's no test case for this. Consider adding:

// Invalid: empty username (whitespace-only key)
_, err = utils.ParseUsersFromMap(map[string]config.UserConfig{
    "   ": {Password: hash},
})
assert.ErrorContains(t, err, "empty username")
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/utils/user_utils_test.go` around lines 147 - 151, Add a test case in
internal/utils/user_utils_test.go next to the existing invalid-password case
that exercises ParseUsersFromMap with a whitespace-only map key to verify
empty-username validation; call utils.ParseUsersFromMap with a
map[string]config.UserConfig containing a key like "   " and a valid Password
(e.g., hash) and assert that the returned error contains "empty username" (use
assert.ErrorContains with the same pattern as the empty-password test).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@internal/service/oidc_service.go`:
- Around line 702-713: The phone and address claims are being emitted as
placeholders even when empty; update the block that checks
slices.Contains(scopes, "phone") to only set userInfo.PhoneNumber and
userInfo.PhoneNumberVerified if user.PhoneNumber is non-empty (set the verified
pointer only when a phone exists), and update the address block to skip setting
userInfo.Address unless user.Address contains real data (e.g., user.Address is
non-empty and json.Unmarshal into config.AddressClaim yields a
non-zero/meaningful struct rather than default/empty {}). Ensure you reference
the existing symbols (scopes check, user.PhoneNumber, user.PhoneNumberVerified,
userInfo.PhoneNumberVerified, user.Address, config.AddressClaim,
userInfo.Address) when making the change.

---

Outside diff comments:
In `@internal/middleware/context_middleware.go`:
- Around line 116-129: When cookie.Provider == "local" after calling localUser
:= m.auth.GetLocalUser(cookie.Username) and setting localAttributes, also prefer
attribute-backed name/email: if localUser.Attributes contains "name" or "email"
use those values to set UserContext.Name and UserContext.Email instead of the
cookie values; then call m.auth.RefreshSessionCookie(c) and create the context
with Username, Name, Email, Provider, IsLoggedIn, LdapGroups and Attributes (so
the c.Set("context", &config.UserContext{...}) block uses the attribute-backed
Name/Email when present).

---

Nitpick comments:
In `@internal/utils/user_utils_test.go`:
- Around line 147-151: Add a test case in internal/utils/user_utils_test.go next
to the existing invalid-password case that exercises ParseUsersFromMap with a
whitespace-only map key to verify empty-username validation; call
utils.ParseUsersFromMap with a map[string]config.UserConfig containing a key
like "   " and a valid Password (e.g., hash) and assert that the returned error
contains "empty username" (use assert.ErrorContains with the same pattern as the
empty-password test).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 9a4466f8-2aa3-4d0d-a8b2-d5f76eeeb699

📥 Commits

Reviewing files that changed from the base of the PR and between 11e5158 and 0f58663.

📒 Files selected for processing (16)
  • frontend/src/lib/i18n/locales/en.json
  • frontend/src/pages/authorize-page.tsx
  • internal/assets/migrations/000008_oidc_userinfo_profile.down.sql
  • internal/assets/migrations/000008_oidc_userinfo_profile.up.sql
  • internal/config/config.go
  • internal/controller/user_controller.go
  • internal/controller/well_known_controller.go
  • internal/controller/well_known_controller_test.go
  • internal/middleware/context_middleware.go
  • internal/repository/models.go
  • internal/repository/oidc_queries.sql.go
  • internal/service/oidc_service.go
  • internal/utils/user_utils.go
  • internal/utils/user_utils_test.go
  • sql/oidc_queries.sql
  • sql/oidc_schemas.sql
✅ Files skipped from review due to trivial changes (6)
  • internal/controller/well_known_controller_test.go
  • internal/controller/well_known_controller.go
  • internal/assets/migrations/000008_oidc_userinfo_profile.down.sql
  • frontend/src/lib/i18n/locales/en.json
  • internal/assets/migrations/000008_oidc_userinfo_profile.up.sql
  • internal/config/config.go
🚧 Files skipped from review as they are similar to previous changes (5)
  • frontend/src/pages/authorize-page.tsx
  • sql/oidc_schemas.sql
  • internal/controller/user_controller.go
  • internal/repository/models.go
  • internal/repository/oidc_queries.sql.go

Comment thread internal/service/oidc_service.go
@scottmckendry scottmckendry force-pushed the oidcc-user-attributes-scopes branch 2 times, most recently from eb40af6 to f7a2ad1 Compare April 9, 2026 17:57
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ Duplicate comments (1)
internal/service/oidc_service.go (1)

702-713: ⚠️ Potential issue | 🟡 Minor

Phone and address claims may emit empty/placeholder values.

The past review suggested omitting these claims when the underlying data is absent, but the current implementation still:

  1. Sets PhoneNumberVerified pointer even when PhoneNumber is empty
  2. Sets Address pointer for any successfully unmarshalled JSON, including {}

This results in responses like "phone_number": "", "phone_number_verified": false or "address": {} when no data exists.

Suggested fix
 	if slices.Contains(scopes, "phone") {
-		userInfo.PhoneNumber = user.PhoneNumber
-		verified := user.PhoneNumberVerified
-		userInfo.PhoneNumberVerified = &verified
+		if user.PhoneNumber != "" {
+			userInfo.PhoneNumber = user.PhoneNumber
+			verified := user.PhoneNumberVerified
+			userInfo.PhoneNumberVerified = &verified
+		}
 	}
 
 	if slices.Contains(scopes, "address") {
 		var addr config.AddressClaim
-		if err := json.Unmarshal([]byte(user.Address), &addr); err == nil {
+		if err := json.Unmarshal([]byte(user.Address), &addr); err == nil &&
+			(addr.Formatted != "" || addr.StreetAddress != "" ||
+				addr.Locality != "" || addr.Region != "" ||
+				addr.PostalCode != "" || addr.Country != "") {
 			userInfo.Address = &addr
 		}
 	}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/service/oidc_service.go` around lines 702 - 713, The phone and
address claims are being emitted even when underlying data is absent; update the
logic in the block that checks slices.Contains(scopes, "phone") to only set
userInfo.PhoneNumber and userInfo.PhoneNumberVerified when user.PhoneNumber is
non-empty (i.e., skip setting the PhoneNumberVerified pointer if PhoneNumber ==
""), and update the address block (the code that unmarshals user.Address into
config.AddressClaim and assigns userInfo.Address) to only assign a non-nil
pointer when the unmarshalled addr contains meaningful fields (e.g., check for
zero/empty fields on the config.AddressClaim struct or use a helper like
addr.IsEmpty() before setting userInfo.Address) so empty strings/{} are not
returned as claims.
🧹 Nitpick comments (1)
internal/utils/user_utils.go (1)

64-88: Consider trimming UserAttributes string fields for consistency.

While Username, Password, and TotpSecret are trimmed, cfg.Attributes is assigned directly. If users accidentally include leading/trailing whitespace in YAML attribute values (e.g., name: " John Doe "), it will be preserved. This is minor since YAML parsers typically handle this, but trimming would ensure consistency with other fields.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/utils/user_utils.go` around lines 64 - 88, In ParseUsersFromMap,
trim whitespace for each attribute value before assigning Attributes: iterate
over cfg.Attributes (e.g., for k, v := range cfg.Attributes), call
strings.TrimSpace on v, build a cleaned map (preserving keys) and set
Attributes: cleanedMap on the config.User being appended; this mirrors the
trimming applied to Username, Password, and TotpSecret and ensures attribute
string fields don't retain accidental leading/trailing whitespace.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@internal/service/oidc_service.go`:
- Around line 702-713: The phone and address claims are being emitted even when
underlying data is absent; update the logic in the block that checks
slices.Contains(scopes, "phone") to only set userInfo.PhoneNumber and
userInfo.PhoneNumberVerified when user.PhoneNumber is non-empty (i.e., skip
setting the PhoneNumberVerified pointer if PhoneNumber == ""), and update the
address block (the code that unmarshals user.Address into config.AddressClaim
and assigns userInfo.Address) to only assign a non-nil pointer when the
unmarshalled addr contains meaningful fields (e.g., check for zero/empty fields
on the config.AddressClaim struct or use a helper like addr.IsEmpty() before
setting userInfo.Address) so empty strings/{} are not returned as claims.

---

Nitpick comments:
In `@internal/utils/user_utils.go`:
- Around line 64-88: In ParseUsersFromMap, trim whitespace for each attribute
value before assigning Attributes: iterate over cfg.Attributes (e.g., for k, v
:= range cfg.Attributes), call strings.TrimSpace on v, build a cleaned map
(preserving keys) and set Attributes: cleanedMap on the config.User being
appended; this mirrors the trimming applied to Username, Password, and
TotpSecret and ensures attribute string fields don't retain accidental
leading/trailing whitespace.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: b7dc363c-795d-425c-8103-7a30c77e229e

📥 Commits

Reviewing files that changed from the base of the PR and between eb40af6 and f7a2ad1.

📒 Files selected for processing (16)
  • frontend/src/lib/i18n/locales/en.json
  • frontend/src/pages/authorize-page.tsx
  • internal/assets/migrations/000008_oidc_userinfo_profile.down.sql
  • internal/assets/migrations/000008_oidc_userinfo_profile.up.sql
  • internal/config/config.go
  • internal/controller/user_controller.go
  • internal/controller/well_known_controller.go
  • internal/controller/well_known_controller_test.go
  • internal/middleware/context_middleware.go
  • internal/repository/models.go
  • internal/repository/oidc_queries.sql.go
  • internal/service/oidc_service.go
  • internal/utils/user_utils.go
  • internal/utils/user_utils_test.go
  • sql/oidc_queries.sql
  • sql/oidc_schemas.sql
✅ Files skipped from review due to trivial changes (5)
  • internal/assets/migrations/000008_oidc_userinfo_profile.down.sql
  • internal/controller/well_known_controller_test.go
  • internal/assets/migrations/000008_oidc_userinfo_profile.up.sql
  • internal/repository/models.go
  • internal/middleware/context_middleware.go
🚧 Files skipped from review as they are similar to previous changes (4)
  • internal/controller/well_known_controller.go
  • internal/repository/oidc_queries.sql.go
  • sql/oidc_schemas.sql
  • internal/utils/user_utils_test.go

@dosubot dosubot Bot added size:XXL This PR changes 1000+ lines, ignoring generated files. and removed size:XL This PR changes 500-999 lines, ignoring generated files. labels Apr 9, 2026
@scottmckendry scottmckendry force-pushed the oidcc-user-attributes-scopes branch 2 times, most recently from 613bfa2 to 9bb49da Compare April 9, 2026 18:35
@scottmckendry scottmckendry marked this pull request as draft April 9, 2026 22:15
@scottmckendry scottmckendry force-pushed the oidcc-user-attributes-scopes branch from 9bb49da to db9e0b5 Compare April 11, 2026 05:02
@scottmckendry scottmckendry marked this pull request as ready for review April 11, 2026 05:03
@dosubot dosubot Bot added size:XL This PR changes 500-999 lines, ignoring generated files. and removed size:XXL This PR changes 1000+ lines, ignoring generated files. labels Apr 11, 2026
@scottmckendry scottmckendry force-pushed the oidcc-user-attributes-scopes branch 3 times, most recently from 15a4c8c to d483c62 Compare April 11, 2026 06:38
@Rycochet
Copy link
Copy Markdown
Collaborator

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 11, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Comment thread internal/utils/loaders/loader_file.go Outdated
@scottmckendry scottmckendry force-pushed the oidcc-user-attributes-scopes branch from d483c62 to 868a2b9 Compare April 11, 2026 09:17
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

♻️ Duplicate comments (1)
internal/service/oidc_service.go (1)

707-717: ⚠️ Potential issue | 🟡 Minor

Omit empty phone/address claims instead of returning placeholders.

When phone is requested for a user with no stored phone number, this still emits phone_number_verified:false. Likewise, the zero-value address serialized by StoreUserinfo() becomes "{}", which unmarshals successfully here and returns address:{}. Both claims should be omitted when the underlying value is absent.

Suggested fix
 	if slices.Contains(scopes, "phone") {
-		userInfo.PhoneNumber = user.PhoneNumber
-		verified := user.PhoneNumberVerified != 0
-		userInfo.PhoneNumberVerified = &verified
+		if user.PhoneNumber != "" {
+			userInfo.PhoneNumber = user.PhoneNumber
+			verified := user.PhoneNumberVerified != 0
+			userInfo.PhoneNumberVerified = &verified
+		}
 	}
 
 	if slices.Contains(scopes, "address") {
 		var addr config.AddressClaim
-		if err := json.Unmarshal([]byte(user.Address), &addr); err == nil {
+		if err := json.Unmarshal([]byte(user.Address), &addr); err == nil &&
+			(addr.Formatted != "" ||
+				addr.StreetAddress != "" ||
+				addr.Locality != "" ||
+				addr.Region != "" ||
+				addr.PostalCode != "" ||
+				addr.Country != "") {
 			userInfo.Address = &addr
 		}
 	}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/service/oidc_service.go` around lines 707 - 717, The code currently
sets phone and address claims even when underlying values are absent; change the
phone block to only set userInfo.PhoneNumber and userInfo.PhoneNumberVerified
when user.PhoneNumber is non-empty (e.g., if strings.TrimSpace(user.PhoneNumber)
!= "") and set the verified pointer only in that case; change the address block
to skip setting userInfo.Address when user.Address is empty or when
unmarshalling yields a zero-value config.AddressClaim (e.g., unmarshal into addr
and then check reflect.DeepEqual(addr, config.AddressClaim{}) before assigning)
so empty "{}" or empty strings result in omitted claims; update the
slices.Contains(scopes, "phone") and slices.Contains(scopes, "address") branches
accordingly and keep json.Unmarshal error handling as-is.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@frontend/src/pages/authorize-page.tsx`:
- Around line 64-75: The new scope strings phoneScopeName,
phoneScopeDescription, addressScopeName, and addressScopeDescription referenced
in authorize-page (used where id: "phone" and id: "address" are defined) are
only present in en.json; add these same keys to all other locale JSON files used
by the app with appropriate translations (or safe fallbacks) so non-English
users see translated UI text—ensure key names match exactly and update each
locale file in frontend locales set.

In `@internal/config/config.go`:
- Around line 118-145: BootstrapApp.Setup() currently logs app.config and
app.context.users including raw PII from the new UserAttributes/AddressClaim and
User.Attributes; sanitize or exclude those before logging by producing a copy of
the config and user list with UserAttributes (and User.Attributes) removed or
redacted (e.g., set to nil/zero or replace sensitive fields with placeholders)
prior to the trace log call in BootstrapApp.Setup(), or implement a
Redact/Normalized method on UserAttributes/AddressClaim and call it on each user
before logging.

In `@internal/controller/user_controller_test.go`:
- Around line 362-381: Tests currently set Attributes directly on config.User
which bypasses the new Auth.UserAttributes -> utils.GetUsers() path; update the
test setup so users' attributes are provided via the same config source that
utils.GetUsers() reads (i.e., seed them into service.AuthServiceConfig.Users /
the auth config used by utils.GetUsers() rather than directly assigning
config.User.Attributes), then exercise the real login flow (including the TOTP
branch) and assert that the saved session or pending-session
(config.UserContext) contains the Name and Email values propagated from that
config; ensure both the normal login subtest and the TOTP subtest verify
persisted session/pending-session Name/Email rather than just checking cookie
presence.

In `@internal/service/oidc_service.go`:
- Around line 69-90: The UserinfoResponse currently emits email_verified (and
phone_number_verified) without backing state; update CompileUserinfo() to only
set/emit UserinfoResponse.EmailVerified and PhoneNumberVerified when there is an
explicit stored verification flag on the user (e.g.,
userAttributes.EmailVerified / userAttributes.PhoneNumberVerified) or omit those
claims entirely if unknown; locate the CompileUserinfo function and the
UserinfoResponse struct and change the logic that unconditionally sets
email_verified=true to read the verification booleans from the user record (or
skip adding the field) so relying parties only see verified=true when a real
verification flag exists.

---

Duplicate comments:
In `@internal/service/oidc_service.go`:
- Around line 707-717: The code currently sets phone and address claims even
when underlying values are absent; change the phone block to only set
userInfo.PhoneNumber and userInfo.PhoneNumberVerified when user.PhoneNumber is
non-empty (e.g., if strings.TrimSpace(user.PhoneNumber) != "") and set the
verified pointer only in that case; change the address block to skip setting
userInfo.Address when user.Address is empty or when unmarshalling yields a
zero-value config.AddressClaim (e.g., unmarshal into addr and then check
reflect.DeepEqual(addr, config.AddressClaim{}) before assigning) so empty "{}"
or empty strings result in omitted claims; update the slices.Contains(scopes,
"phone") and slices.Contains(scopes, "address") branches accordingly and keep
json.Unmarshal error handling as-is.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 3061e1c2-8741-4590-97c4-ac1c4437e3f4

📥 Commits

Reviewing files that changed from the base of the PR and between eb40af6 and d483c62.

📒 Files selected for processing (20)
  • frontend/src/lib/i18n/locales/en.json
  • frontend/src/pages/authorize-page.tsx
  • internal/assets/migrations/000008_oidc_userinfo_profile.down.sql
  • internal/assets/migrations/000008_oidc_userinfo_profile.up.sql
  • internal/bootstrap/app_bootstrap.go
  • internal/config/config.go
  • internal/controller/user_controller.go
  • internal/controller/user_controller_test.go
  • internal/controller/well_known_controller.go
  • internal/controller/well_known_controller_test.go
  • internal/middleware/context_middleware.go
  • internal/repository/models.go
  • internal/repository/oidc_queries.sql.go
  • internal/service/oidc_service.go
  • internal/service/oidc_service_test.go
  • internal/utils/loaders/loader_file.go
  • internal/utils/user_utils.go
  • internal/utils/user_utils_test.go
  • sql/oidc_queries.sql
  • sql/oidc_schemas.sql
✅ Files skipped from review due to trivial changes (5)
  • internal/utils/loaders/loader_file.go
  • internal/assets/migrations/000008_oidc_userinfo_profile.down.sql
  • internal/controller/well_known_controller_test.go
  • internal/assets/migrations/000008_oidc_userinfo_profile.up.sql
  • internal/repository/models.go
🚧 Files skipped from review as they are similar to previous changes (9)
  • internal/controller/well_known_controller.go
  • frontend/src/lib/i18n/locales/en.json
  • internal/utils/user_utils_test.go
  • internal/utils/user_utils.go
  • sql/oidc_schemas.sql
  • sql/oidc_queries.sql
  • internal/repository/oidc_queries.sql.go
  • internal/middleware/context_middleware.go
  • internal/controller/user_controller.go

Comment thread frontend/src/pages/authorize-page.tsx
Comment thread internal/config/config.go
Comment thread internal/controller/user_controller_test.go Outdated
Comment thread internal/service/oidc_service.go
Copy link
Copy Markdown
Member

@steveiliop56 steveiliop56 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks really good! Some small feedback and we can merge.

Comment thread internal/config/config.go
Comment thread internal/config/config.go
Comment thread internal/service/oidc_service.go Outdated
Comment thread internal/service/oidc_service_test.go
Comment thread internal/controller/user_controller_test.go Outdated
@scottmckendry scottmckendry force-pushed the oidcc-user-attributes-scopes branch from 6517df9 to 29a8fd4 Compare April 12, 2026 19:51
@dosubot dosubot Bot added size:L This PR changes 100-499 lines, ignoring generated files. and removed size:XL This PR changes 500-999 lines, ignoring generated files. labels Apr 12, 2026
@scottmckendry scottmckendry force-pushed the oidcc-user-attributes-scopes branch from 29a8fd4 to 44c8d3c Compare April 12, 2026 19:53
@scottmckendry
Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 12, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
internal/config/config.go (1)

146-153: Consider adding IsEmpty() helper method for AddressClaim.

To support checking whether an address has meaningful data (useful in CompileUserinfo to avoid emitting "address": {}), a helper method could be added:

Suggested helper
func (a AddressClaim) IsEmpty() bool {
	return a.Formatted == "" &&
		a.StreetAddress == "" &&
		a.Locality == "" &&
		a.Region == "" &&
		a.PostalCode == "" &&
		a.Country == ""
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/config/config.go` around lines 146 - 153, Add an IsEmpty helper on
the AddressClaim type to let callers detect when the address has no meaningful
fields so CompileUserinfo (and other callers) can skip emitting an empty
"address" object; implement AddressClaim.IsEmpty() to return true when all
fields (Formatted, StreetAddress, Locality, Region, PostalCode, Country) are
empty strings and update usages (e.g., CompileUserinfo) to call a.IsEmpty()
before emitting the address.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@internal/config/config.go`:
- Around line 146-153: Add an IsEmpty helper on the AddressClaim type to let
callers detect when the address has no meaningful fields so CompileUserinfo (and
other callers) can skip emitting an empty "address" object; implement
AddressClaim.IsEmpty() to return true when all fields (Formatted, StreetAddress,
Locality, Region, PostalCode, Country) are empty strings and update usages
(e.g., CompileUserinfo) to call a.IsEmpty() before emitting the address.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 4270d289-045f-4f0c-ab56-b94a7ddef234

📥 Commits

Reviewing files that changed from the base of the PR and between d483c62 and 44c8d3c.

📒 Files selected for processing (19)
  • frontend/src/lib/i18n/locales/en.json
  • frontend/src/pages/authorize-page.tsx
  • internal/assets/migrations/000008_oidc_userinfo_profile.down.sql
  • internal/assets/migrations/000008_oidc_userinfo_profile.up.sql
  • internal/bootstrap/app_bootstrap.go
  • internal/config/config.go
  • internal/controller/user_controller.go
  • internal/controller/user_controller_test.go
  • internal/controller/well_known_controller.go
  • internal/controller/well_known_controller_test.go
  • internal/middleware/context_middleware.go
  • internal/repository/models.go
  • internal/repository/oidc_queries.sql.go
  • internal/service/oidc_service.go
  • internal/service/oidc_service_test.go
  • internal/utils/user_utils.go
  • internal/utils/user_utils_test.go
  • sql/oidc_queries.sql
  • sql/oidc_schemas.sql
✅ Files skipped from review due to trivial changes (7)
  • internal/bootstrap/app_bootstrap.go
  • internal/assets/migrations/000008_oidc_userinfo_profile.down.sql
  • frontend/src/pages/authorize-page.tsx
  • internal/controller/well_known_controller_test.go
  • internal/repository/models.go
  • internal/service/oidc_service_test.go
  • frontend/src/lib/i18n/locales/en.json
🚧 Files skipped from review as they are similar to previous changes (8)
  • internal/controller/well_known_controller.go
  • internal/assets/migrations/000008_oidc_userinfo_profile.up.sql
  • sql/oidc_schemas.sql
  • internal/repository/oidc_queries.sql.go
  • sql/oidc_queries.sql
  • internal/utils/user_utils.go
  • internal/middleware/context_middleware.go
  • internal/controller/user_controller.go

Copy link
Copy Markdown
Member

@steveiliop56 steveiliop56 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Final comments. Can you please confirm that attributes are set in the remote- headers in the authentication response?

Comment thread internal/assets/migrations/000008_oidc_userinfo_profile.down.sql Outdated
Comment thread internal/controller/user_controller.go
@scottmckendry scottmckendry force-pushed the oidcc-user-attributes-scopes branch from 7fae43e to 75c0f33 Compare April 17, 2026 08:04
@steveiliop56
Copy link
Copy Markdown
Member

@scottmckendry would you like to solve conflicts or should I? It's just the go module rename.

@scottmckendry scottmckendry force-pushed the oidcc-user-attributes-scopes branch from 8b1fd66 to 6510920 Compare April 26, 2026 19:41
@scottmckendry
Copy link
Copy Markdown
Member Author

@steveiliop56 - good to go. I've left in my forked branch because I'm not sure how to change upstream without having to re-open the PR.. and this one has quite a bit of history 😁

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:L This PR changes 100-499 lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants