Compare commits

..

6 Commits

Author SHA1 Message Date
Stavros
1d76cb84b9 New Crowdin updates (#684)
* New translations en.json (Korean)

* New translations en.json (Dutch)

* New translations en.json (Chinese Simplified)
2026-03-04 19:41:21 +02:00
dependabot[bot]
e8c8343fcf chore(deps): bump the minor-patch group in /frontend with 2 updates (#688)
Bumps the minor-patch group in /frontend with 2 updates: [i18next](https://github.com/i18next/i18next) and [rollup-plugin-visualizer](https://github.com/btd/rollup-plugin-visualizer).


Updates `i18next` from 25.8.13 to 25.8.14
- [Release notes](https://github.com/i18next/i18next/releases)
- [Changelog](https://github.com/i18next/i18next/blob/master/CHANGELOG.md)
- [Commits](https://github.com/i18next/i18next/compare/v25.8.13...v25.8.14)

Updates `rollup-plugin-visualizer` from 7.0.0 to 7.0.1
- [Changelog](https://github.com/btd/rollup-plugin-visualizer/blob/master/CHANGELOG.md)
- [Commits](https://github.com/btd/rollup-plugin-visualizer/compare/v7.0.0...v7.0.1)

---
updated-dependencies:
- dependency-name: i18next
  dependency-version: 25.8.14
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: minor-patch
- dependency-name: rollup-plugin-visualizer
  dependency-version: 7.0.1
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: minor-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-04 19:40:40 +02:00
dependabot[bot]
a7d7959a82 chore(deps): bump github.com/weppos/publicsuffix-go (#687)
Bumps the minor-patch group with 1 update: [github.com/weppos/publicsuffix-go](https://github.com/weppos/publicsuffix-go).


Updates `github.com/weppos/publicsuffix-go` from 0.50.2 to 0.50.3
- [Changelog](https://github.com/weppos/publicsuffix-go/blob/main/CHANGELOG.md)
- [Commits](https://github.com/weppos/publicsuffix-go/compare/v0.50.2...v0.50.3)

---
updated-dependencies:
- dependency-name: github.com/weppos/publicsuffix-go
  dependency-version: 0.50.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: minor-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-04 19:40:11 +02:00
Stavros
69c6c0ba1d fix: add cache control header to token response 2026-03-04 19:38:52 +02:00
Stavros
a71f61df8d feat: add email verified claim 2026-03-04 15:52:31 +02:00
Stavros
6bf444010b feat: add nonce claim support to oidc server (#686)
* feat: add nonce claim support to oidc server

* fix: review feedback
2026-03-04 15:34:11 +02:00
19 changed files with 184 additions and 134 deletions

View File

@@ -16,7 +16,7 @@
"axios": "^1.13.6",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"i18next": "^25.8.13",
"i18next": "^25.8.14",
"i18next-browser-languagedetector": "^8.2.1",
"i18next-resources-to-backend": "^1.2.1",
"input-otp": "^1.4.2",
@@ -46,7 +46,7 @@
"eslint-plugin-react-refresh": "^0.5.2",
"globals": "^17.4.0",
"prettier": "3.8.1",
"rollup-plugin-visualizer": "^7.0.0",
"rollup-plugin-visualizer": "^7.0.1",
"tw-animate-css": "^1.4.0",
"typescript": "~5.9.3",
"typescript-eslint": "^8.56.1",
@@ -637,7 +637,7 @@
"html-url-attributes": ["html-url-attributes@3.0.1", "", {}, "sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ=="],
"i18next": ["i18next@25.8.13", "", { "dependencies": { "@babel/runtime": "^7.28.4" }, "peerDependencies": { "typescript": "^5" }, "optionalPeers": ["typescript"] }, "sha512-E0vzjBY1yM+nsFrtgkjLhST2NBkirkvOVoQa0MSldhsuZ3jUge7ZNpuwG0Cfc74zwo5ZwRzg3uOgT+McBn32iA=="],
"i18next": ["i18next@25.8.14", "", { "dependencies": { "@babel/runtime": "^7.28.4" }, "peerDependencies": { "typescript": "^5" }, "optionalPeers": ["typescript"] }, "sha512-paMUYkfWJMsWPeE/Hejcw+XLhHrQPehem+4wMo+uELnvIwvCG019L9sAIljwjCmEMtFQQO3YeitJY8Kctei3iA=="],
"i18next-browser-languagedetector": ["i18next-browser-languagedetector@8.2.1", "", { "dependencies": { "@babel/runtime": "^7.23.2" } }, "sha512-bZg8+4bdmaOiApD7N7BPT9W8MLZG+nPTOFlLiJiT8uzKXFjhxw4v2ierCXOwB5sFDMtuA5G4kgYZ0AznZxQ/cw=="],
@@ -863,7 +863,7 @@
"rollup": ["rollup@4.46.2", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.46.2", "@rollup/rollup-android-arm64": "4.46.2", "@rollup/rollup-darwin-arm64": "4.46.2", "@rollup/rollup-darwin-x64": "4.46.2", "@rollup/rollup-freebsd-arm64": "4.46.2", "@rollup/rollup-freebsd-x64": "4.46.2", "@rollup/rollup-linux-arm-gnueabihf": "4.46.2", "@rollup/rollup-linux-arm-musleabihf": "4.46.2", "@rollup/rollup-linux-arm64-gnu": "4.46.2", "@rollup/rollup-linux-arm64-musl": "4.46.2", "@rollup/rollup-linux-loongarch64-gnu": "4.46.2", "@rollup/rollup-linux-ppc64-gnu": "4.46.2", "@rollup/rollup-linux-riscv64-gnu": "4.46.2", "@rollup/rollup-linux-riscv64-musl": "4.46.2", "@rollup/rollup-linux-s390x-gnu": "4.46.2", "@rollup/rollup-linux-x64-gnu": "4.46.2", "@rollup/rollup-linux-x64-musl": "4.46.2", "@rollup/rollup-win32-arm64-msvc": "4.46.2", "@rollup/rollup-win32-ia32-msvc": "4.46.2", "@rollup/rollup-win32-x64-msvc": "4.46.2", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-WMmLFI+Boh6xbop+OAGo9cQ3OgX9MIg7xOQjn+pTCwOkk+FNDAeAemXkJ3HzDJrVXleLOFVa1ipuc1AmEx1Dwg=="],
"rollup-plugin-visualizer": ["rollup-plugin-visualizer@7.0.0", "", { "dependencies": { "open": "^11.0.0", "picomatch": "^4.0.2", "source-map": "^0.7.4", "yargs": "^18.0.0" }, "peerDependencies": { "rolldown": "1.x || ^1.0.0-beta || ^1.0.0-rc", "rollup": "2.x || 3.x || 4.x" }, "optionalPeers": ["rolldown", "rollup"], "bin": { "rollup-plugin-visualizer": "dist/bin/cli.js" } }, "sha512-loo4kmhTg7GMO0hqaUv/azvLPUT2B4jXU3gNMG35gm1mWKpOzhV6rspb/Mqmsfg7oOTdkzdmOckCIwGB5Ca1CA=="],
"rollup-plugin-visualizer": ["rollup-plugin-visualizer@7.0.1", "", { "dependencies": { "open": "^11.0.0", "picomatch": "^4.0.2", "source-map": "^0.7.4", "yargs": "^18.0.0" }, "peerDependencies": { "rolldown": "1.x || ^1.0.0-beta || ^1.0.0-rc", "rollup": "2.x || 3.x || 4.x" }, "optionalPeers": ["rolldown", "rollup"], "bin": { "rollup-plugin-visualizer": "dist/bin/cli.js" } }, "sha512-UJUT4+1Ho4OcWmPYU3sYXgUqI8B8Ayfe06MX7y0qCJ1K8aGoKtR/NDd/2nZqM7ADkrzny+I99Ul7GgyoiVNAgg=="],
"run-applescript": ["run-applescript@7.1.0", "", {}, "sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q=="],

View File

@@ -22,7 +22,7 @@
"axios": "^1.13.6",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"i18next": "^25.8.13",
"i18next": "^25.8.14",
"i18next-browser-languagedetector": "^8.2.1",
"i18next-resources-to-backend": "^1.2.1",
"input-otp": "^1.4.2",
@@ -52,7 +52,7 @@
"eslint-plugin-react-refresh": "^0.5.2",
"globals": "^17.4.0",
"prettier": "3.8.1",
"rollup-plugin-visualizer": "^7.0.0",
"rollup-plugin-visualizer": "^7.0.1",
"tw-animate-css": "^1.4.0",
"typescript": "~5.9.3",
"typescript-eslint": "^8.56.1",

View File

@@ -4,6 +4,7 @@ export type OIDCValues = {
client_id: string;
redirect_uri: string;
state: string;
nonce: string;
};
interface IuseOIDCParams {
@@ -13,7 +14,7 @@ interface IuseOIDCParams {
missingParams: string[];
}
const optionalParams: string[] = ["state"];
const optionalParams: string[] = ["state", "nonce"];
export function useOIDCParams(params: URLSearchParams): IuseOIDCParams {
let compiled: string = "";
@@ -26,6 +27,7 @@ export function useOIDCParams(params: URLSearchParams): IuseOIDCParams {
client_id: params.get("client_id") ?? "",
redirect_uri: params.get("redirect_uri") ?? "",
state: params.get("state") ?? "",
nonce: params.get("nonce") ?? "",
};
for (const key of Object.keys(values)) {

View File

@@ -1,83 +1,83 @@
{
"loginTitle": "Welcome back, login with",
"loginTitleSimple": "Welcome back, please login",
"loginDivider": "Or",
"loginUsername": "Username",
"loginPassword": "Password",
"loginSubmit": "Login",
"loginFailTitle": "Failed to log in",
"loginFailSubtitle": "Please check your username and password",
"loginFailRateLimit": "You failed to login too many times. Please try again later",
"loginSuccessTitle": "Logged in",
"loginSuccessSubtitle": "Welcome back!",
"loginOauthFailTitle": "An error occurred",
"loginOauthFailSubtitle": "Failed to get OAuth URL",
"loginOauthSuccessTitle": "Redirecting",
"loginOauthSuccessSubtitle": "Redirecting to your OAuth provider",
"loginOauthAutoRedirectTitle": "OAuth Auto Redirect",
"loginOauthAutoRedirectSubtitle": "You will be automatically redirected to your OAuth provider to authenticate.",
"loginOauthAutoRedirectButton": "Redirect now",
"continueTitle": "Continue",
"continueRedirectingTitle": "Redirecting...",
"continueRedirectingSubtitle": "You should be redirected to the app soon",
"continueRedirectManually": "Redirect me manually",
"continueInsecureRedirectTitle": "Insecure redirect",
"continueInsecureRedirectSubtitle": "You are trying to redirect from <code>https</code> to <code>http</code> which is not secure. Are you sure you want to continue?",
"continueUntrustedRedirectTitle": "Untrusted redirect",
"continueUntrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain (<code>{{cookieDomain}}</code>). Are you sure you want to continue?",
"logoutFailTitle": "Failed to log out",
"logoutFailSubtitle": "Please try again",
"logoutSuccessTitle": "Logged out",
"logoutSuccessSubtitle": "You have been logged out",
"logoutTitle": "Logout",
"logoutUsernameSubtitle": "You are currently logged in as <code>{{username}}</code>. Click the button below to logout.",
"logoutOauthSubtitle": "You are currently logged in as <code>{{username}}</code> using the {{provider}} OAuth provider. Click the button below to logout.",
"notFoundTitle": "Page not found",
"notFoundSubtitle": "The page you are looking for does not exist.",
"notFoundButton": "Go home",
"totpFailTitle": "Failed to verify code",
"totpFailSubtitle": "Please check your code and try again",
"totpSuccessTitle": "Verified",
"totpSuccessSubtitle": "Redirecting to your app",
"totpTitle": "Enter your TOTP code",
"totpSubtitle": "Please enter the code from your authenticator app.",
"unauthorizedTitle": "Unauthorized",
"unauthorizedResourceSubtitle": "The user with username <code>{{username}}</code> is not authorized to access the resource <code>{{resource}}</code>.",
"unauthorizedLoginSubtitle": "The user with username <code>{{username}}</code> is not authorized to login.",
"unauthorizedGroupsSubtitle": "The user with username <code>{{username}}</code> is not in the groups required by the resource <code>{{resource}}</code>.",
"unauthorizedIpSubtitle": "Your IP address <code>{{ip}}</code> is not authorized to access the resource <code>{{resource}}</code>.",
"unauthorizedButton": "Try again",
"cancelTitle": "Cancel",
"forgotPasswordTitle": "Forgot your password?",
"failedToFetchProvidersTitle": "Failed to load authentication providers. Please check your configuration.",
"errorTitle": "An error occurred",
"errorSubtitleInfo": "The following error occurred while processing your request:",
"loginTitle": "다시 오신 것을 환영합니다. 아래 방법으로 로그인하세요",
"loginTitleSimple": "다시 오신 것을 환영합니다. 로그인해 주세요",
"loginDivider": "또는",
"loginUsername": "사용자 이름",
"loginPassword": "비밀번호",
"loginSubmit": "로그인",
"loginFailTitle": "로그인 실패",
"loginFailSubtitle": "사용자 이름과 비밀번호를 확인해 주세요",
"loginFailRateLimit": "로그인을 너무 많이 시도했습니다. 나중에 다시 시도해 주세요",
"loginSuccessTitle": "로그인 성공",
"loginSuccessSubtitle": "다시 오신 것을 환영합니다!",
"loginOauthFailTitle": "오류가 발생했습니다",
"loginOauthFailSubtitle": "OAuth URL을 가져오는 데 실패했습니다",
"loginOauthSuccessTitle": "리디렉션 중",
"loginOauthSuccessSubtitle": "OAuth 제공자로 리디렉션 중입니다",
"loginOauthAutoRedirectTitle": "OAuth 자동 리디렉션",
"loginOauthAutoRedirectSubtitle": "인증을 위해 OAuth 제공자로 자동 리디렉션됩니다.",
"loginOauthAutoRedirectButton": "지금 리디렉션",
"continueTitle": "계속",
"continueRedirectingTitle": "리디렉션 중...",
"continueRedirectingSubtitle": "곧 앱으로 리디렉션됩니다",
"continueRedirectManually": "직접 리디렉션하기",
"continueInsecureRedirectTitle": "안전하지 않은 리디렉션",
"continueInsecureRedirectSubtitle": "<code>https</code>에서 <code>http</code>로 리디렉션하려고 합니다. 이는 안전하지 않습니다. 계속하시겠습니까?",
"continueUntrustedRedirectTitle": "신뢰할 수 없는 리디렉션",
"continueUntrustedRedirectSubtitle": "설정된 도메인(<code>{{cookieDomain}}</code>)과 일치하지 않는 도메인으로 리디렉션하려고 합니다. 계속하시겠습니까?",
"logoutFailTitle": "로그아웃 실패",
"logoutFailSubtitle": "다시 시도해 주세요",
"logoutSuccessTitle": "로그아웃 완료",
"logoutSuccessSubtitle": "로그아웃되었습니다",
"logoutTitle": "로그아웃",
"logoutUsernameSubtitle": "현재 <code>{{username}}</code>(으)로 로그인되어 있습니다. 아래 버튼을 클릭하여 로그아웃하세요.",
"logoutOauthSubtitle": "현재 {{provider}} OAuth 제공자를 통해 <code>{{username}}</code>(으)로 로그인되어 있습니다. 아래 버튼을 클릭하여 로그아웃하세요.",
"notFoundTitle": "페이지를 찾을 수 없습니다",
"notFoundSubtitle": "찾으시는 페이지가 존재하지 않습니다.",
"notFoundButton": "홈으로 가기",
"totpFailTitle": "코드 확인 실패",
"totpFailSubtitle": "코드를 확인하고 다시 시도해 주세요",
"totpSuccessTitle": "확인 완료",
"totpSuccessSubtitle": "앱으로 리디렉션 중입니다",
"totpTitle": "TOTP 코드 입력",
"totpSubtitle": "인증 앱의 코드를 입력해 주세요.",
"unauthorizedTitle": "권한 없음",
"unauthorizedResourceSubtitle": "사용자 이름 <code>{{username}}</code>은(는) 리소스 <code>{{resource}}</code>에 접근할 권한이 없습니다.",
"unauthorizedLoginSubtitle": "사용자 이름 <code>{{username}}</code>은(는) 로그인할 권한이 없습니다.",
"unauthorizedGroupsSubtitle": "사용자 이름 <code>{{username}}</code>은(는) 리소스 <code>{{resource}}</code>에서 요구하는 그룹에 속해 있지 않습니다.",
"unauthorizedIpSubtitle": "IP 주소 <code>{{ip}}</code>는 리소스 <code>{{resource}}</code>에 접근할 권한이 없습니다.",
"unauthorizedButton": "다시 시도",
"cancelTitle": "취소",
"forgotPasswordTitle": "비밀번호를 잊으셨나요?",
"failedToFetchProvidersTitle": "인증 제공자를 불러오는 데 실패했습니다. 설정을 확인해 주세요.",
"errorTitle": "오류가 발생했습니다",
"errorSubtitleInfo": "요청 처리 중 다음 오류가 발생했습니다:",
"errorSubtitle": "An error occurred while trying to perform this action. Please check the console for more information.",
"forgotPasswordMessage": "You can reset your password by changing the `USERS` environment variable.",
"fieldRequired": "This field is required",
"invalidInput": "Invalid input",
"domainWarningTitle": "Invalid Domain",
"domainWarningSubtitle": "This instance is configured to be accessed from <code>{{appUrl}}</code>, but <code>{{currentUrl}}</code> is being used. If you proceed, you may encounter issues with authentication.",
"domainWarningCurrent": "Current:",
"domainWarningExpected": "Expected:",
"ignoreTitle": "Ignore",
"goToCorrectDomainTitle": "Go to correct domain",
"authorizeTitle": "Authorize",
"authorizeCardTitle": "Continue to {{app}}?",
"authorizeSubtitle": "Would you like to continue to this app? Please carefully review the permissions requested by the app.",
"authorizeSubtitleOAuth": "Would you like to continue to this app?",
"authorizeLoadingTitle": "Loading...",
"authorizeLoadingSubtitle": "Please wait while we load the client information.",
"authorizeSuccessTitle": "Authorized",
"authorizeSuccessSubtitle": "You will be redirected to the app in a few seconds.",
"authorizeErrorClientInfo": "An error occurred while loading the client information. Please try again later.",
"authorizeErrorMissingParams": "The following parameters are missing: {{missingParams}}",
"forgotPasswordMessage": "USERS 환경 변수를 변경하여 비밀번호를 재설정할 수 있습니다.",
"fieldRequired": "필수 입력 항목입니다",
"invalidInput": "잘못된 입력입니다",
"domainWarningTitle": "잘못된 도메인",
"domainWarningSubtitle": "잘못된 도메인에서 이 인스턴스에 접근하고 있습니다. 계속 진행하면 인증 문제가 발생할 수 있습니다.",
"domainWarningCurrent": "현재:",
"domainWarningExpected": "예상:",
"ignoreTitle": "무시",
"goToCorrectDomainTitle": "올바른 도메인으로 이동",
"authorizeTitle": "권한 부여",
"authorizeCardTitle": "{{app}}(으)로 계속하시겠습니까?",
"authorizeSubtitle": "이 앱으로 계속하시겠습니까? 앱에서 요청한 권한을 주의 깊게 검토해 주세요.",
"authorizeSubtitleOAuth": "이 앱으로 계속하시겠습니까?",
"authorizeLoadingTitle": "로딩 중...",
"authorizeLoadingSubtitle": "클라이언트 정보를 불러오는 동안 기다려 주세요.",
"authorizeSuccessTitle": "권한 부여 완료",
"authorizeSuccessSubtitle": "몇 초 후에 앱으로 리디렉션됩니다.",
"authorizeErrorClientInfo": "클라이언트 정보를 불러오는 중 오류가 발생했습니다. 나중에 다시 시도해 주세요.",
"authorizeErrorMissingParams": "다음 매개변수가 누락되었습니다: {{missingParams}}",
"openidScopeName": "OpenID Connect",
"openidScopeDescription": "Allows the app to access your OpenID Connect information.",
"emailScopeName": "Email",
"emailScopeDescription": "Allows the app to access your email address.",
"profileScopeName": "Profile",
"profileScopeDescription": "Allows the app to access your profile information.",
"groupsScopeName": "Groups",
"groupsScopeDescription": "Allows the app to access your group information."
"openidScopeDescription": "앱이 OpenID Connect 정보에 접근할 수 있도록 허용합니다.",
"emailScopeName": "이메일",
"emailScopeDescription": "앱이 이메일 주소에 접근할 수 있도록 허용합니다.",
"profileScopeName": "프로필",
"profileScopeDescription": "앱이 프로필 정보에 접근할 수 있도록 허용합니다.",
"groupsScopeName": "그룹",
"groupsScopeDescription": "앱이 그룹 정보에 접근할 수 있도록 허용합니다."
}

View File

@@ -58,8 +58,8 @@
"invalidInput": "Ongeldige invoer",
"domainWarningTitle": "Ongeldig domein",
"domainWarningSubtitle": "Deze instantie is geconfigureerd voor toegang tot <code>{{appUrl}}</code>, maar <code>{{currentUrl}}</code> wordt gebruikt. Als je doorgaat, kun je problemen ondervinden met authenticatie.",
"domainWarningCurrent": "Huidige:",
"domainWarningExpected": "Verwachte:",
"domainWarningCurrent": "Huidig:",
"domainWarningExpected": "Verwacht:",
"ignoreTitle": "Negeren",
"goToCorrectDomainTitle": "Ga naar het juiste domein",
"authorizeTitle": "Autoriseren",

View File

@@ -54,12 +54,12 @@
"errorSubtitleInfo": "处理您的请求时发生了以下错误:",
"errorSubtitle": "执行此操作时发生错误,请检查控制台以获取更多信息。",
"forgotPasswordMessage": "您可以通过更改 `USERS ` 环境变量重置您的密码。",
"fieldRequired": "必字段",
"fieldRequired": "必字段",
"invalidInput": "无效的输入",
"domainWarningTitle": "无效域名",
"domainWarningSubtitle": "当前实例配置的访问地址为 <code>{{appUrl}}</code>,但您正在使用 <code>{{currentUrl}}</code>。若继续操作,可能会遇到身份验证问题。",
"domainWarningCurrent": "Current:",
"domainWarningExpected": "Expected:",
"domainWarningSubtitle": "您正在从一个错误的域名访问此实例。如继续,您可能会遇到身份验证问题。",
"domainWarningCurrent": "当前:",
"domainWarningExpected": "预期:",
"ignoreTitle": "忽略",
"goToCorrectDomainTitle": "转到正确的域名",
"authorizeTitle": "授权",

View File

@@ -98,6 +98,7 @@ export const AuthorizePage = () => {
client_id: props.client_id,
redirect_uri: props.redirect_uri,
state: props.state,
nonce: props.nonce,
});
},
mutationKey: ["authorize", props.client_id],

2
go.mod
View File

@@ -18,7 +18,7 @@ require (
github.com/pquerna/otp v1.5.0
github.com/rs/zerolog v1.34.0
github.com/traefik/paerser v0.2.2
github.com/weppos/publicsuffix-go v0.50.2
github.com/weppos/publicsuffix-go v0.50.3
golang.org/x/crypto v0.48.0
golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546
golang.org/x/oauth2 v0.35.0

4
go.sum
View File

@@ -275,8 +275,8 @@ github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
github.com/ugorji/go/codec v1.3.1 h1:waO7eEiFDwidsBN6agj1vJQ4AG7lh2yqXyOXqhgQuyY=
github.com/ugorji/go/codec v1.3.1/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4=
github.com/weppos/publicsuffix-go v0.50.2 h1:KsJFc8IEKTJovM46SRCnGNsM+rFShxcs6VEHjOJcXzE=
github.com/weppos/publicsuffix-go v0.50.2/go.mod h1:CbQCKDtXF8UcT7hrxeMa0MDjwhpOI9iYOU7cfq+yo8k=
github.com/weppos/publicsuffix-go v0.50.3 h1:eT5dcjHQcVDNc0igpFEsGHKIip30feuB2zuuI9eJxiE=
github.com/weppos/publicsuffix-go v0.50.3/go.mod h1:/rOa781xBykZhHK/I3QeHo92qdDKVmKZKF7s8qAEM/4=
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=

View File

@@ -0,0 +1,2 @@
ALTER TABLE "oidc_codes" DROP COLUMN "nonce";
ALTER TABLE "oidc_tokens" DROP COLUMN "nonce";

View File

@@ -0,0 +1,2 @@
ALTER TABLE "oidc_codes" ADD COLUMN "nonce" TEXT DEFAULT "";
ALTER TABLE "oidc_tokens" ADD COLUMN "nonce" TEXT DEFAULT "";

View File

@@ -231,7 +231,7 @@ func (controller *OIDCController) Token(c *gin.Context) {
if !ok {
tlog.App.Error().Msg("Missing authorization header")
c.Header("www-authenticate", "basic")
c.JSON(401, gin.H{
c.JSON(400, gin.H{
"error": "invalid_client",
})
return
@@ -296,7 +296,7 @@ func (controller *OIDCController) Token(c *gin.Context) {
return
}
tokenRes, err := controller.oidc.GenerateAccessToken(c, client, entry.Sub, entry.Scope)
tokenRes, err := controller.oidc.GenerateAccessToken(c, client, entry)
if err != nil {
tlog.App.Error().Err(err).Msg("Failed to generate access token")
@@ -313,7 +313,7 @@ func (controller *OIDCController) Token(c *gin.Context) {
if err != nil {
if errors.Is(err, service.ErrTokenExpired) {
tlog.App.Error().Err(err).Msg("Refresh token expired")
c.JSON(401, gin.H{
c.JSON(400, gin.H{
"error": "invalid_grant",
})
return
@@ -321,7 +321,7 @@ func (controller *OIDCController) Token(c *gin.Context) {
if errors.Is(err, service.ErrInvalidClient) {
tlog.App.Error().Err(err).Msg("Invalid client")
c.JSON(401, gin.H{
c.JSON(400, gin.H{
"error": "invalid_grant",
})
return
@@ -337,6 +337,9 @@ func (controller *OIDCController) Token(c *gin.Context) {
tokenResponse = tokenRes
}
c.Header("cache-control", "no-store")
c.Header("pragma", "no-cache")
c.JSON(200, tokenResponse)
}

View File

@@ -59,7 +59,7 @@ func (controller *WellKnownController) OpenIDConnectConfiguration(c *gin.Context
SubjectTypesSupported: []string{"pairwise"},
IDTokenSigningAlgValuesSupported: []string{"RS256"},
TokenEndpointAuthMethodsSupported: []string{"client_secret_basic", "client_secret_post"},
ClaimsSupported: []string{"sub", "updated_at", "name", "preferred_username", "email", "groups"},
ClaimsSupported: []string{"sub", "updated_at", "name", "preferred_username", "email", "email_verified", "groups"},
ServiceDocumentation: "https://tinyauth.app/docs/guides/oidc",
})
}

View File

@@ -11,6 +11,7 @@ type OidcCode struct {
RedirectURI string
ClientID string
ExpiresAt int64
Nonce string
}
type OidcToken struct {
@@ -21,6 +22,7 @@ type OidcToken struct {
ClientID string
TokenExpiresAt int64
RefreshTokenExpiresAt int64
Nonce string
}
type OidcUserinfo struct {

View File

@@ -16,11 +16,12 @@ INSERT INTO "oidc_codes" (
"scope",
"redirect_uri",
"client_id",
"expires_at"
"expires_at",
"nonce"
) VALUES (
?, ?, ?, ?, ?, ?
?, ?, ?, ?, ?, ?, ?
)
RETURNING sub, code_hash, scope, redirect_uri, client_id, expires_at
RETURNING sub, code_hash, scope, redirect_uri, client_id, expires_at, nonce
`
type CreateOidcCodeParams struct {
@@ -30,6 +31,7 @@ type CreateOidcCodeParams struct {
RedirectURI string
ClientID string
ExpiresAt int64
Nonce string
}
func (q *Queries) CreateOidcCode(ctx context.Context, arg CreateOidcCodeParams) (OidcCode, error) {
@@ -40,6 +42,7 @@ func (q *Queries) CreateOidcCode(ctx context.Context, arg CreateOidcCodeParams)
arg.RedirectURI,
arg.ClientID,
arg.ExpiresAt,
arg.Nonce,
)
var i OidcCode
err := row.Scan(
@@ -49,6 +52,7 @@ func (q *Queries) CreateOidcCode(ctx context.Context, arg CreateOidcCodeParams)
&i.RedirectURI,
&i.ClientID,
&i.ExpiresAt,
&i.Nonce,
)
return i, err
}
@@ -61,11 +65,12 @@ INSERT INTO "oidc_tokens" (
"scope",
"client_id",
"token_expires_at",
"refresh_token_expires_at"
"refresh_token_expires_at",
"nonce"
) VALUES (
?, ?, ?, ?, ?, ?, ?
?, ?, ?, ?, ?, ?, ?, ?
)
RETURNING sub, access_token_hash, refresh_token_hash, scope, client_id, token_expires_at, refresh_token_expires_at
RETURNING sub, access_token_hash, refresh_token_hash, scope, client_id, token_expires_at, refresh_token_expires_at, nonce
`
type CreateOidcTokenParams struct {
@@ -76,6 +81,7 @@ type CreateOidcTokenParams struct {
ClientID string
TokenExpiresAt int64
RefreshTokenExpiresAt int64
Nonce string
}
func (q *Queries) CreateOidcToken(ctx context.Context, arg CreateOidcTokenParams) (OidcToken, error) {
@@ -87,6 +93,7 @@ func (q *Queries) CreateOidcToken(ctx context.Context, arg CreateOidcTokenParams
arg.ClientID,
arg.TokenExpiresAt,
arg.RefreshTokenExpiresAt,
arg.Nonce,
)
var i OidcToken
err := row.Scan(
@@ -97,6 +104,7 @@ func (q *Queries) CreateOidcToken(ctx context.Context, arg CreateOidcTokenParams
&i.ClientID,
&i.TokenExpiresAt,
&i.RefreshTokenExpiresAt,
&i.Nonce,
)
return i, err
}
@@ -148,7 +156,7 @@ func (q *Queries) CreateOidcUserInfo(ctx context.Context, arg CreateOidcUserInfo
const deleteExpiredOidcCodes = `-- name: DeleteExpiredOidcCodes :many
DELETE FROM "oidc_codes"
WHERE "expires_at" < ?
RETURNING sub, code_hash, scope, redirect_uri, client_id, expires_at
RETURNING sub, code_hash, scope, redirect_uri, client_id, expires_at, nonce
`
func (q *Queries) DeleteExpiredOidcCodes(ctx context.Context, expiresAt int64) ([]OidcCode, error) {
@@ -167,6 +175,7 @@ func (q *Queries) DeleteExpiredOidcCodes(ctx context.Context, expiresAt int64) (
&i.RedirectURI,
&i.ClientID,
&i.ExpiresAt,
&i.Nonce,
); err != nil {
return nil, err
}
@@ -184,7 +193,7 @@ func (q *Queries) DeleteExpiredOidcCodes(ctx context.Context, expiresAt int64) (
const deleteExpiredOidcTokens = `-- name: DeleteExpiredOidcTokens :many
DELETE FROM "oidc_tokens"
WHERE "token_expires_at" < ? AND "refresh_token_expires_at" < ?
RETURNING sub, access_token_hash, refresh_token_hash, scope, client_id, token_expires_at, refresh_token_expires_at
RETURNING sub, access_token_hash, refresh_token_hash, scope, client_id, token_expires_at, refresh_token_expires_at, nonce
`
type DeleteExpiredOidcTokensParams struct {
@@ -209,6 +218,7 @@ func (q *Queries) DeleteExpiredOidcTokens(ctx context.Context, arg DeleteExpired
&i.ClientID,
&i.TokenExpiresAt,
&i.RefreshTokenExpiresAt,
&i.Nonce,
); err != nil {
return nil, err
}
@@ -276,7 +286,7 @@ func (q *Queries) DeleteOidcUserInfo(ctx context.Context, sub string) error {
const getOidcCode = `-- name: GetOidcCode :one
DELETE FROM "oidc_codes"
WHERE "code_hash" = ?
RETURNING sub, code_hash, scope, redirect_uri, client_id, expires_at
RETURNING sub, code_hash, scope, redirect_uri, client_id, expires_at, nonce
`
func (q *Queries) GetOidcCode(ctx context.Context, codeHash string) (OidcCode, error) {
@@ -289,6 +299,7 @@ func (q *Queries) GetOidcCode(ctx context.Context, codeHash string) (OidcCode, e
&i.RedirectURI,
&i.ClientID,
&i.ExpiresAt,
&i.Nonce,
)
return i, err
}
@@ -296,7 +307,7 @@ func (q *Queries) GetOidcCode(ctx context.Context, codeHash string) (OidcCode, e
const getOidcCodeBySub = `-- name: GetOidcCodeBySub :one
DELETE FROM "oidc_codes"
WHERE "sub" = ?
RETURNING sub, code_hash, scope, redirect_uri, client_id, expires_at
RETURNING sub, code_hash, scope, redirect_uri, client_id, expires_at, nonce
`
func (q *Queries) GetOidcCodeBySub(ctx context.Context, sub string) (OidcCode, error) {
@@ -309,12 +320,13 @@ func (q *Queries) GetOidcCodeBySub(ctx context.Context, sub string) (OidcCode, e
&i.RedirectURI,
&i.ClientID,
&i.ExpiresAt,
&i.Nonce,
)
return i, err
}
const getOidcCodeBySubUnsafe = `-- name: GetOidcCodeBySubUnsafe :one
SELECT sub, code_hash, scope, redirect_uri, client_id, expires_at FROM "oidc_codes"
SELECT sub, code_hash, scope, redirect_uri, client_id, expires_at, nonce FROM "oidc_codes"
WHERE "sub" = ?
`
@@ -328,12 +340,13 @@ func (q *Queries) GetOidcCodeBySubUnsafe(ctx context.Context, sub string) (OidcC
&i.RedirectURI,
&i.ClientID,
&i.ExpiresAt,
&i.Nonce,
)
return i, err
}
const getOidcCodeUnsafe = `-- name: GetOidcCodeUnsafe :one
SELECT sub, code_hash, scope, redirect_uri, client_id, expires_at FROM "oidc_codes"
SELECT sub, code_hash, scope, redirect_uri, client_id, expires_at, nonce FROM "oidc_codes"
WHERE "code_hash" = ?
`
@@ -347,12 +360,13 @@ func (q *Queries) GetOidcCodeUnsafe(ctx context.Context, codeHash string) (OidcC
&i.RedirectURI,
&i.ClientID,
&i.ExpiresAt,
&i.Nonce,
)
return i, err
}
const getOidcToken = `-- name: GetOidcToken :one
SELECT sub, access_token_hash, refresh_token_hash, scope, client_id, token_expires_at, refresh_token_expires_at FROM "oidc_tokens"
SELECT sub, access_token_hash, refresh_token_hash, scope, client_id, token_expires_at, refresh_token_expires_at, nonce FROM "oidc_tokens"
WHERE "access_token_hash" = ?
`
@@ -367,12 +381,13 @@ func (q *Queries) GetOidcToken(ctx context.Context, accessTokenHash string) (Oid
&i.ClientID,
&i.TokenExpiresAt,
&i.RefreshTokenExpiresAt,
&i.Nonce,
)
return i, err
}
const getOidcTokenByRefreshToken = `-- name: GetOidcTokenByRefreshToken :one
SELECT sub, access_token_hash, refresh_token_hash, scope, client_id, token_expires_at, refresh_token_expires_at FROM "oidc_tokens"
SELECT sub, access_token_hash, refresh_token_hash, scope, client_id, token_expires_at, refresh_token_expires_at, nonce FROM "oidc_tokens"
WHERE "refresh_token_hash" = ?
`
@@ -387,12 +402,13 @@ func (q *Queries) GetOidcTokenByRefreshToken(ctx context.Context, refreshTokenHa
&i.ClientID,
&i.TokenExpiresAt,
&i.RefreshTokenExpiresAt,
&i.Nonce,
)
return i, err
}
const getOidcTokenBySub = `-- name: GetOidcTokenBySub :one
SELECT sub, access_token_hash, refresh_token_hash, scope, client_id, token_expires_at, refresh_token_expires_at FROM "oidc_tokens"
SELECT sub, access_token_hash, refresh_token_hash, scope, client_id, token_expires_at, refresh_token_expires_at, nonce FROM "oidc_tokens"
WHERE "sub" = ?
`
@@ -407,6 +423,7 @@ func (q *Queries) GetOidcTokenBySub(ctx context.Context, sub string) (OidcToken,
&i.ClientID,
&i.TokenExpiresAt,
&i.RefreshTokenExpiresAt,
&i.Nonce,
)
return i, err
}
@@ -437,7 +454,7 @@ UPDATE "oidc_tokens" SET
"token_expires_at" = ?,
"refresh_token_expires_at" = ?
WHERE "refresh_token_hash" = ?
RETURNING sub, access_token_hash, refresh_token_hash, scope, client_id, token_expires_at, refresh_token_expires_at
RETURNING sub, access_token_hash, refresh_token_hash, scope, client_id, token_expires_at, refresh_token_expires_at, nonce
`
type UpdateOidcTokenByRefreshTokenParams struct {
@@ -465,6 +482,7 @@ func (q *Queries) UpdateOidcTokenByRefreshToken(ctx context.Context, arg UpdateO
&i.ClientID,
&i.TokenExpiresAt,
&i.RefreshTokenExpiresAt,
&i.Nonce,
)
return i, err
}

View File

@@ -49,16 +49,19 @@ type ClaimSet struct {
Exp int64 `json:"exp"`
Name string `json:"name,omitempty"`
Email string `json:"email,omitempty"`
EmailVerified bool `json:"email_verified,omitempty"`
PreferredUsername string `json:"preferred_username,omitempty"`
Groups []string `json:"groups,omitempty"`
Nonce string `json:"nonce,omitempty"`
}
type UserinfoResponse struct {
Sub string `json:"sub"`
Name string `json:"name"`
Email string `json:"email"`
PreferredUsername string `json:"preferred_username"`
Name string `json:"name,omitempty"`
Email string `json:"email,omitempty"`
PreferredUsername string `json:"preferred_username,omitempty"`
Groups []string `json:"groups,omitempty"`
EmailVerified bool `json:"email_verified,omitempty"`
UpdatedAt int64 `json:"updated_at"`
}
@@ -77,6 +80,7 @@ type AuthorizeRequest struct {
ClientID string `json:"client_id" binding:"required"`
RedirectURI string `json:"redirect_uri" binding:"required"`
State string `json:"state" binding:"required"`
Nonce string `json:"nonce"`
}
type OIDCServiceConfig struct {
@@ -212,6 +216,9 @@ func (service *OIDCService) Init() error {
for id, client := range service.config.Clients {
client.ID = id
if client.Name == "" {
client.Name = utils.Capitalize(client.ID)
}
service.clients[client.ClientID] = client
}
@@ -293,6 +300,7 @@ func (service *OIDCService) StoreCode(c *gin.Context, sub string, code string, r
RedirectURI: req.RedirectURI,
ClientID: req.ClientID,
ExpiresAt: expiresAt,
Nonce: req.Nonce,
})
return err
@@ -354,7 +362,7 @@ func (service *OIDCService) GetCodeEntry(c *gin.Context, codeHash string) (repos
return oidcCode, nil
}
func (service *OIDCService) generateIDToken(client config.OIDCClientConfig, user repository.OidcUserinfo, scope string) (string, error) {
func (service *OIDCService) generateIDToken(client config.OIDCClientConfig, user repository.OidcUserinfo, scope string, nonce string) (string, error) {
createdAt := time.Now().Unix()
expiresAt := time.Now().Add(time.Duration(service.config.SessionExpiry) * time.Second).Unix()
@@ -382,8 +390,10 @@ func (service *OIDCService) generateIDToken(client config.OIDCClientConfig, user
Exp: expiresAt,
Name: userInfo.Name,
Email: userInfo.Email,
EmailVerified: userInfo.EmailVerified,
PreferredUsername: userInfo.PreferredUsername,
Groups: userInfo.Groups,
Nonce: nonce,
}
payload, err := json.Marshal(claims)
@@ -407,14 +417,14 @@ func (service *OIDCService) generateIDToken(client config.OIDCClientConfig, user
return token, nil
}
func (service *OIDCService) GenerateAccessToken(c *gin.Context, client config.OIDCClientConfig, sub string, scope string) (TokenResponse, error) {
user, err := service.GetUserinfo(c, sub)
func (service *OIDCService) GenerateAccessToken(c *gin.Context, client config.OIDCClientConfig, codeEntry repository.OidcCode) (TokenResponse, error) {
user, err := service.GetUserinfo(c, codeEntry.Sub)
if err != nil {
return TokenResponse{}, err
}
idToken, err := service.generateIDToken(client, user, scope)
idToken, err := service.generateIDToken(client, user, codeEntry.Scope, codeEntry.Nonce)
if err != nil {
return TokenResponse{}, err
@@ -434,17 +444,18 @@ func (service *OIDCService) GenerateAccessToken(c *gin.Context, client config.OI
TokenType: "Bearer",
ExpiresIn: int64(service.config.SessionExpiry),
IDToken: idToken,
Scope: strings.ReplaceAll(scope, ",", " "),
Scope: strings.ReplaceAll(codeEntry.Scope, ",", " "),
}
_, err = service.queries.CreateOidcToken(c, repository.CreateOidcTokenParams{
Sub: sub,
Sub: codeEntry.Sub,
AccessTokenHash: service.Hash(accessToken),
RefreshTokenHash: service.Hash(refreshToken),
ClientID: client.ClientID,
Scope: scope,
Scope: codeEntry.Scope,
TokenExpiresAt: tokenExpiresAt,
RefreshTokenExpiresAt: refrshTokenExpiresAt,
Nonce: codeEntry.Nonce,
})
if err != nil {
@@ -481,7 +492,7 @@ func (service *OIDCService) RefreshAccessToken(c *gin.Context, refreshToken stri
idToken, err := service.generateIDToken(config.OIDCClientConfig{
ClientID: entry.ClientID,
}, user, entry.Scope)
}, user, entry.Scope, entry.Nonce)
if err != nil {
return TokenResponse{}, err
@@ -575,6 +586,8 @@ func (service *OIDCService) CompileUserinfo(user repository.OidcUserinfo, scope
if slices.Contains(scopes, "email") {
userInfo.Email = user.Email
// We can set this as a configuration option in the future but for now it's a good idea to assume it's true
userInfo.EmailVerified = true
}
if slices.Contains(scopes, "groups") {

View File

@@ -5,9 +5,10 @@ INSERT INTO "oidc_codes" (
"scope",
"redirect_uri",
"client_id",
"expires_at"
"expires_at",
"nonce"
) VALUES (
?, ?, ?, ?, ?, ?
?, ?, ?, ?, ?, ?, ?
)
RETURNING *;
@@ -45,9 +46,10 @@ INSERT INTO "oidc_tokens" (
"scope",
"client_id",
"token_expires_at",
"refresh_token_expires_at"
"refresh_token_expires_at",
"nonce"
) VALUES (
?, ?, ?, ?, ?, ?, ?
?, ?, ?, ?, ?, ?, ?, ?
)
RETURNING *;
@@ -72,7 +74,6 @@ WHERE "refresh_token_hash" = ?;
SELECT * FROM "oidc_tokens"
WHERE "sub" = ?;
-- name: DeleteOidcToken :exec
DELETE FROM "oidc_tokens"
WHERE "access_token_hash" = ?;

View File

@@ -4,7 +4,8 @@ CREATE TABLE IF NOT EXISTS "oidc_codes" (
"scope" TEXT NOT NULL,
"redirect_uri" TEXT NOT NULL,
"client_id" TEXT NOT NULL,
"expires_at" INTEGER NOT NULL
"expires_at" INTEGER NOT NULL,
"nonce" TEXT DEFAULT ""
);
CREATE TABLE IF NOT EXISTS "oidc_tokens" (
@@ -14,7 +15,8 @@ CREATE TABLE IF NOT EXISTS "oidc_tokens" (
"scope" TEXT NOT NULL,
"client_id" TEXT NOT NULL,
"token_expires_at" INTEGER NOT NULL,
"refresh_token_expires_at" INTEGER NOT NULL
"refresh_token_expires_at" INTEGER NOT NULL,
"nonce" TEXT DEFAULT ""
);
CREATE TABLE IF NOT EXISTS "oidc_userinfo" (

View File

@@ -22,3 +22,7 @@ sql:
go_type: "string"
- column: "sessions.ldap_groups"
go_type: "string"
- column: "oidc_codes.nonce"
go_type: "string"
- column: "oidc_tokens.nonce"
go_type: "string"