Skip to content

Commit eb40af6

Browse files
committed
feat(oidc): support for all in-spec attributes and scopes
1 parent 0d286d1 commit eb40af6

16 files changed

Lines changed: 522 additions & 140 deletions

frontend/src/lib/i18n/locales/en.json

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,5 +79,9 @@
7979
"profileScopeName": "Profile",
8080
"profileScopeDescription": "Allows the app to access your profile information.",
8181
"groupsScopeName": "Groups",
82-
"groupsScopeDescription": "Allows the app to access your group information."
82+
"groupsScopeDescription": "Allows the app to access your group information.",
83+
"phoneScopeName": "Phone",
84+
"phoneScopeDescription": "Allows the app to access your phone number.",
85+
"addressScopeName": "Address",
86+
"addressScopeDescription": "Allows the app to access your address."
8387
}

frontend/src/pages/authorize-page.tsx

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ import { toast } from "sonner";
1717
import { useOIDCParams } from "@/lib/hooks/oidc";
1818
import { useTranslation } from "react-i18next";
1919
import { TFunction } from "i18next";
20-
import { Mail, Shield, User, Users } from "lucide-react";
20+
import { Mail, MapPin, Phone, Shield, User, Users } from "lucide-react";
2121
import {
2222
Tooltip,
2323
TooltipContent,
@@ -61,6 +61,18 @@ const createScopeMap = (t: TFunction<"translation", undefined>): Scope[] => {
6161
description: t("groupsScopeDescription"),
6262
icon: <Users {...scopeMapIconProps} />,
6363
},
64+
{
65+
id: "phone",
66+
name: t("phoneScopeName"),
67+
description: t("phoneScopeDescription"),
68+
icon: <Phone {...scopeMapIconProps} />,
69+
},
70+
{
71+
id: "address",
72+
name: t("addressScopeName"),
73+
description: t("addressScopeDescription"),
74+
icon: <MapPin {...scopeMapIconProps} />,
75+
},
6476
];
6577
};
6678

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
ALTER TABLE "oidc_userinfo" DROP COLUMN "given_name";
2+
ALTER TABLE "oidc_userinfo" DROP COLUMN "family_name";
3+
ALTER TABLE "oidc_userinfo" DROP COLUMN "middle_name";
4+
ALTER TABLE "oidc_userinfo" DROP COLUMN "nickname";
5+
ALTER TABLE "oidc_userinfo" DROP COLUMN "profile";
6+
ALTER TABLE "oidc_userinfo" DROP COLUMN "picture";
7+
ALTER TABLE "oidc_userinfo" DROP COLUMN "website";
8+
ALTER TABLE "oidc_userinfo" DROP COLUMN "gender";
9+
ALTER TABLE "oidc_userinfo" DROP COLUMN "birthdate";
10+
ALTER TABLE "oidc_userinfo" DROP COLUMN "zoneinfo";
11+
ALTER TABLE "oidc_userinfo" DROP COLUMN "locale";
12+
ALTER TABLE "oidc_userinfo" DROP COLUMN "phone_number";
13+
ALTER TABLE "oidc_userinfo" DROP COLUMN "phone_number_verified";
14+
ALTER TABLE "oidc_userinfo" DROP COLUMN "address";
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
ALTER TABLE "oidc_userinfo" ADD COLUMN "given_name" TEXT NOT NULL DEFAULT "";
2+
ALTER TABLE "oidc_userinfo" ADD COLUMN "family_name" TEXT NOT NULL DEFAULT "";
3+
ALTER TABLE "oidc_userinfo" ADD COLUMN "middle_name" TEXT NOT NULL DEFAULT "";
4+
ALTER TABLE "oidc_userinfo" ADD COLUMN "nickname" TEXT NOT NULL DEFAULT "";
5+
ALTER TABLE "oidc_userinfo" ADD COLUMN "profile" TEXT NOT NULL DEFAULT "";
6+
ALTER TABLE "oidc_userinfo" ADD COLUMN "picture" TEXT NOT NULL DEFAULT "";
7+
ALTER TABLE "oidc_userinfo" ADD COLUMN "website" TEXT NOT NULL DEFAULT "";
8+
ALTER TABLE "oidc_userinfo" ADD COLUMN "gender" TEXT NOT NULL DEFAULT "";
9+
ALTER TABLE "oidc_userinfo" ADD COLUMN "birthdate" TEXT NOT NULL DEFAULT "";
10+
ALTER TABLE "oidc_userinfo" ADD COLUMN "zoneinfo" TEXT NOT NULL DEFAULT "";
11+
ALTER TABLE "oidc_userinfo" ADD COLUMN "locale" TEXT NOT NULL DEFAULT "";
12+
ALTER TABLE "oidc_userinfo" ADD COLUMN "phone_number" TEXT NOT NULL DEFAULT "";
13+
ALTER TABLE "oidc_userinfo" ADD COLUMN "phone_number_verified" INTEGER NOT NULL DEFAULT 0;
14+
ALTER TABLE "oidc_userinfo" ADD COLUMN "address" TEXT NOT NULL DEFAULT "{}";

internal/config/config.go

Lines changed: 45 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -113,15 +113,49 @@ type ServerConfig struct {
113113
}
114114

115115
type AuthConfig struct {
116-
IP IPConfig `description:"IP whitelisting config options." yaml:"ip"`
117-
Users []string `description:"Comma-separated list of users (username:hashed_password)." yaml:"users"`
118-
UsersFile string `description:"Path to the users file." yaml:"usersFile"`
119-
SecureCookie bool `description:"Enable secure cookies." yaml:"secureCookie"`
120-
SessionExpiry int `description:"Session expiry time in seconds." yaml:"sessionExpiry"`
121-
SessionMaxLifetime int `description:"Maximum session lifetime in seconds." yaml:"sessionMaxLifetime"`
122-
LoginTimeout int `description:"Login timeout in seconds." yaml:"loginTimeout"`
123-
LoginMaxRetries int `description:"Maximum login retries." yaml:"loginMaxRetries"`
124-
TrustedProxies []string `description:"Comma-separated list of trusted proxy addresses." yaml:"trustedProxies"`
116+
IP IPConfig `description:"IP whitelisting config options." yaml:"ip"`
117+
Users map[string]UserConfig `description:"Map of users (username -> user config)." yaml:"users"`
118+
UsersFile string `description:"Path to the users file." yaml:"usersFile"`
119+
SecureCookie bool `description:"Enable secure cookies." yaml:"secureCookie"`
120+
SessionExpiry int `description:"Session expiry time in seconds." yaml:"sessionExpiry"`
121+
SessionMaxLifetime int `description:"Maximum session lifetime in seconds." yaml:"sessionMaxLifetime"`
122+
LoginTimeout int `description:"Login timeout in seconds." yaml:"loginTimeout"`
123+
LoginMaxRetries int `description:"Maximum login retries." yaml:"loginMaxRetries"`
124+
TrustedProxies []string `description:"Comma-separated list of trusted proxy addresses." yaml:"trustedProxies"`
125+
}
126+
127+
type UserConfig struct {
128+
Password string `description:"Bcrypt hashed password." yaml:"password"`
129+
TotpSecret string `description:"TOTP secret for two-factor authentication." yaml:"totpSecret"`
130+
Attributes UserAttributes `description:"Optional user attributes used as OIDC claims." yaml:"attributes"`
131+
}
132+
133+
type UserAttributes struct {
134+
Name string `description:"Full name of the user." yaml:"name"`
135+
GivenName string `description:"Given (first) name of the user." yaml:"givenName"`
136+
FamilyName string `description:"Family (last) name of the user." yaml:"familyName"`
137+
MiddleName string `description:"Middle name of the user." yaml:"middleName"`
138+
Nickname string `description:"Nickname of the user." yaml:"nickname"`
139+
Profile string `description:"URL of the user's profile page." yaml:"profile"`
140+
Picture string `description:"URL of the user's profile picture." yaml:"picture"`
141+
Website string `description:"URL of the user's website." yaml:"website"`
142+
Email string `description:"Email address of the user." yaml:"email"`
143+
Gender string `description:"Gender of the user." yaml:"gender"`
144+
Birthdate string `description:"Birthdate of the user (YYYY-MM-DD)." yaml:"birthdate"`
145+
Zoneinfo string `description:"Time zone of the user (e.g. Europe/Athens)." yaml:"zoneinfo"`
146+
Locale string `description:"Locale of the user (e.g. en-US)." yaml:"locale"`
147+
PhoneNumber string `description:"Phone number of the user." yaml:"phoneNumber"`
148+
PhoneNumberVerified bool `description:"Whether the phone number has been verified." yaml:"phoneNumberVerified"`
149+
Address AddressClaim `description:"Address of the user." yaml:"address"`
150+
}
151+
152+
type AddressClaim struct {
153+
Formatted string `description:"Full mailing address, formatted for display." yaml:"formatted" json:"formatted,omitempty"`
154+
StreetAddress string `description:"Street address." yaml:"streetAddress" json:"street_address,omitempty"`
155+
Locality string `description:"City or locality." yaml:"locality" json:"locality,omitempty"`
156+
Region string `description:"State, province, or region." yaml:"region" json:"region,omitempty"`
157+
PostalCode string `description:"Zip or postal code." yaml:"postalCode" json:"postal_code,omitempty"`
158+
Country string `description:"Country." yaml:"country" json:"country,omitempty"`
125159
}
126160

127161
type IPConfig struct {
@@ -228,6 +262,7 @@ type User struct {
228262
Username string
229263
Password string
230264
TotpSecret string
265+
Attributes UserAttributes
231266
}
232267

233268
type LdapUser struct {
@@ -254,6 +289,7 @@ type UserContext struct {
254289
OAuthName string
255290
OAuthSub string
256291
LdapGroups string
292+
Attributes UserAttributes
257293
}
258294

259295
// API responses and queries

internal/controller/user_controller.go

Lines changed: 36 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"fmt"
55
"time"
66

7+
"github.com/steveiliop56/tinyauth/internal/config"
78
"github.com/steveiliop56/tinyauth/internal/repository"
89
"github.com/steveiliop56/tinyauth/internal/service"
910
"github.com/steveiliop56/tinyauth/internal/utils"
@@ -105,16 +106,32 @@ func (controller *UserController) loginHandler(c *gin.Context) {
105106

106107
controller.auth.RecordLoginAttempt(req.Username, true)
107108

109+
var localUser *config.User
108110
if userSearch.Type == "local" {
109-
user := controller.auth.GetLocalUser(userSearch.Username)
111+
u := controller.auth.GetLocalUser(userSearch.Username)
112+
localUser = &u
113+
}
114+
115+
if userSearch.Type == "local" && localUser != nil {
116+
user := *localUser
110117

111118
if user.TotpSecret != "" {
112119
tlog.App.Debug().Str("username", req.Username).Msg("User has TOTP enabled, requiring TOTP verification")
113120

121+
name := user.Attributes.Name
122+
if name == "" {
123+
name = utils.Capitalize(user.Username)
124+
}
125+
126+
email := user.Attributes.Email
127+
if email == "" {
128+
email = utils.CompileUserEmail(user.Username, controller.config.CookieDomain)
129+
}
130+
114131
err := controller.auth.CreateSessionCookie(c, &repository.Session{
115132
Username: user.Username,
116-
Name: utils.Capitalize(user.Username),
117-
Email: utils.CompileUserEmail(user.Username, controller.config.CookieDomain),
133+
Name: name,
134+
Email: email,
118135
Provider: "local",
119136
TotpPending: true,
120137
})
@@ -144,6 +161,15 @@ func (controller *UserController) loginHandler(c *gin.Context) {
144161
Provider: "local",
145162
}
146163

164+
if userSearch.Type == "local" && localUser != nil {
165+
if localUser.Attributes.Name != "" {
166+
sessionCookie.Name = localUser.Attributes.Name
167+
}
168+
if localUser.Attributes.Email != "" {
169+
sessionCookie.Email = localUser.Attributes.Email
170+
}
171+
}
172+
147173
if userSearch.Type == "ldap" {
148174
sessionCookie.Provider = "ldap"
149175
}
@@ -258,6 +284,13 @@ func (controller *UserController) totpHandler(c *gin.Context) {
258284
Provider: "local",
259285
}
260286

287+
if user.Attributes.Name != "" {
288+
sessionCookie.Name = user.Attributes.Name
289+
}
290+
if user.Attributes.Email != "" {
291+
sessionCookie.Email = user.Attributes.Email
292+
}
293+
261294
tlog.App.Trace().Interface("session_cookie", sessionCookie).Msg("Creating session cookie")
262295

263296
err = controller.auth.CreateSessionCookie(c, &sessionCookie)

internal/controller/well_known_controller.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ func (controller *WellKnownController) OpenIDConnectConfiguration(c *gin.Context
5959
SubjectTypesSupported: []string{"pairwise"},
6060
IDTokenSigningAlgValuesSupported: []string{"RS256"},
6161
TokenEndpointAuthMethodsSupported: []string{"client_secret_basic", "client_secret_post"},
62-
ClaimsSupported: []string{"sub", "updated_at", "name", "preferred_username", "email", "email_verified", "groups"},
62+
ClaimsSupported: []string{"sub", "updated_at", "name", "preferred_username", "email", "email_verified", "groups", "phone_number", "phone_number_verified", "address"},
6363
ServiceDocumentation: "https://tinyauth.app/docs/guides/oidc",
6464
})
6565
}

internal/controller/well_known_controller_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ func TestWellKnownController(t *testing.T) {
6767
SubjectTypesSupported: []string{"pairwise"},
6868
IDTokenSigningAlgValuesSupported: []string{"RS256"},
6969
TokenEndpointAuthMethodsSupported: []string{"client_secret_basic", "client_secret_post"},
70-
ClaimsSupported: []string{"sub", "updated_at", "name", "preferred_username", "email", "email_verified", "groups"},
70+
ClaimsSupported: []string{"sub", "updated_at", "name", "preferred_username", "email", "email_verified", "groups", "phone_number", "phone_number_verified", "address"},
7171
ServiceDocumentation: "https://tinyauth.app/docs/guides/oidc",
7272
}
7373

internal/middleware/context_middleware.go

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,7 @@ func (m *ContextMiddleware) Middleware() gin.HandlerFunc {
9999
}
100100

101101
var ldapGroups []string
102+
var localAttributes config.UserAttributes
102103

103104
if cookie.Provider == "ldap" {
104105
ldapUser, err := m.auth.GetLdapUser(userSearch.Username)
@@ -112,6 +113,11 @@ func (m *ContextMiddleware) Middleware() gin.HandlerFunc {
112113
ldapGroups = ldapUser.Groups
113114
}
114115

116+
if cookie.Provider == "local" {
117+
localUser := m.auth.GetLocalUser(cookie.Username)
118+
localAttributes = localUser.Attributes
119+
}
120+
115121
m.auth.RefreshSessionCookie(c)
116122
c.Set("context", &config.UserContext{
117123
Username: cookie.Username,
@@ -120,6 +126,7 @@ func (m *ContextMiddleware) Middleware() gin.HandlerFunc {
120126
Provider: cookie.Provider,
121127
IsLoggedIn: true,
122128
LdapGroups: strings.Join(ldapGroups, ","),
129+
Attributes: localAttributes,
123130
})
124131
c.Next()
125132
return
@@ -202,13 +209,23 @@ func (m *ContextMiddleware) Middleware() gin.HandlerFunc {
202209
return
203210
}
204211

212+
name := utils.Capitalize(user.Username)
213+
if user.Attributes.Name != "" {
214+
name = user.Attributes.Name
215+
}
216+
email := utils.CompileUserEmail(user.Username, m.config.CookieDomain)
217+
if user.Attributes.Email != "" {
218+
email = user.Attributes.Email
219+
}
220+
205221
c.Set("context", &config.UserContext{
206222
Username: user.Username,
207-
Name: utils.Capitalize(user.Username),
208-
Email: utils.CompileUserEmail(user.Username, m.config.CookieDomain),
223+
Name: name,
224+
Email: email,
209225
Provider: "local",
210226
IsLoggedIn: true,
211227
IsBasicAuth: true,
228+
Attributes: user.Attributes,
212229
})
213230
c.Next()
214231
return

internal/repository/models.go

Lines changed: 20 additions & 6 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)