Compare commits

..

3 Commits

Author SHA1 Message Date
Stavros
627fd05d71 i18n: authorize page error messages 2026-02-01 18:58:03 +02:00
Stavros
fb705eaf07 fix: final review comments 2026-02-01 18:47:18 +02:00
Stavros
673f556fb3 fix: more rabbit nitpicks 2026-02-01 00:16:58 +02:00
8 changed files with 42 additions and 16 deletions

View File

@@ -68,6 +68,8 @@
"authorizeLoadingSubtitle": "Please wait while we load the client information.", "authorizeLoadingSubtitle": "Please wait while we load the client information.",
"authorizeSuccessTitle": "Authorized", "authorizeSuccessTitle": "Authorized",
"authorizeSuccessSubtitle": "You will be redirected to the app in a few seconds.", "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}}",
"openidScopeName": "OpenID Connect", "openidScopeName": "OpenID Connect",
"openidScopeDescription": "Allows the app to access your OpenID Connect information.", "openidScopeDescription": "Allows the app to access your OpenID Connect information.",
"emailScopeName": "Email", "emailScopeName": "Email",

View File

@@ -68,6 +68,8 @@
"authorizeLoadingSubtitle": "Please wait while we load the client information.", "authorizeLoadingSubtitle": "Please wait while we load the client information.",
"authorizeSuccessTitle": "Authorized", "authorizeSuccessTitle": "Authorized",
"authorizeSuccessSubtitle": "You will be redirected to the app in a few seconds.", "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}}",
"openidScopeName": "OpenID Connect", "openidScopeName": "OpenID Connect",
"openidScopeDescription": "Allows the app to access your OpenID Connect information.", "openidScopeDescription": "Allows the app to access your OpenID Connect information.",
"emailScopeName": "Email", "emailScopeName": "Email",

View File

@@ -10,7 +10,7 @@ import {
CardFooter, CardFooter,
CardContent, CardContent,
} from "@/components/ui/card"; } from "@/components/ui/card";
import { getOidcClientInfoScehma } from "@/schemas/oidc-schemas"; import { getOidcClientInfoSchema } from "@/schemas/oidc-schemas";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import axios from "axios"; import axios from "axios";
import { toast } from "sonner"; import { toast } from "sonner";
@@ -73,13 +73,13 @@ export const AuthorizePage = () => {
isOidc, isOidc,
compiled: compiledOIDCParams, compiled: compiledOIDCParams,
} = useOIDCParams(searchParams); } = useOIDCParams(searchParams);
const scopes = props.scope.split(" "); const scopes = props.scope ? props.scope.split(" ").filter(Boolean) : [];
const getClientInfo = useQuery({ const getClientInfo = useQuery({
queryKey: ["client", props.client_id], queryKey: ["client", props.client_id],
queryFn: async () => { queryFn: async () => {
const res = await fetch(`/api/oidc/clients/${props.client_id}`); const res = await fetch(`/api/oidc/clients/${props.client_id}`);
const data = await getOidcClientInfoScehma.parseAsync(await res.json()); const data = await getOidcClientInfoSchema.parseAsync(await res.json());
return data; return data;
}, },
enabled: isOidc, enabled: isOidc,
@@ -109,19 +109,19 @@ export const AuthorizePage = () => {
}, },
}); });
if (!isLoggedIn) {
return <Navigate to={`/login?${compiledOIDCParams}`} replace />;
}
if (missingParams.length > 0) { if (missingParams.length > 0) {
return ( return (
<Navigate <Navigate
to={`/error?error=${encodeURIComponent(`Missing parameters: ${missingParams.join(", ")}`)}`} to={`/error?error=${encodeURIComponent(t("authorizeErrorMissingParams", { missingParams: missingParams.join(", ") }))}`}
replace replace
/> />
); );
} }
if (!isLoggedIn) {
return <Navigate to={`/login?${compiledOIDCParams}`} replace />;
}
if (getClientInfo.isLoading) { if (getClientInfo.isLoading) {
return ( return (
<Card className="min-w-xs sm:min-w-sm"> <Card className="min-w-xs sm:min-w-sm">
@@ -138,7 +138,7 @@ export const AuthorizePage = () => {
if (getClientInfo.isError) { if (getClientInfo.isError) {
return ( return (
<Navigate <Navigate
to={`/error?error=${encodeURIComponent(`Failed to load client information`)}`} to={`/error?error=${encodeURIComponent(t("authorizeErrorClientInfo"))}`}
replace replace
/> />
); );

View File

@@ -90,7 +90,9 @@ export const LoginPage = () => {
mutationKey: ["login"], mutationKey: ["login"],
onSuccess: (data) => { onSuccess: (data) => {
if (data.data.totpPending) { if (data.data.totpPending) {
window.location.replace(`/totp?${compiledOIDCParams}`); window.location.replace(
`/totp?redirect_uri=${encodeURIComponent(props.redirect_uri)}`,
);
return; return;
} }
@@ -149,6 +151,10 @@ export const LoginPage = () => {
[], [],
); );
if (isLoggedIn && isOidc) {
return <Navigate to={`/authorize?${compiledOIDCParams}`} replace />;
}
if (isLoggedIn && props.redirect_uri !== "") { if (isLoggedIn && props.redirect_uri !== "") {
return ( return (
<Navigate <Navigate

View File

@@ -1,5 +1,5 @@
import { z } from "zod"; import { z } from "zod";
export const getOidcClientInfoScehma = z.object({ export const getOidcClientInfoSchema = z.object({
name: z.string(), name: z.string(),
}); });

View File

@@ -233,14 +233,14 @@ func (controller *OIDCController) Token(c *gin.Context) {
entry, err := controller.oidc.GetCodeEntry(c, controller.oidc.Hash(req.Code)) entry, err := controller.oidc.GetCodeEntry(c, controller.oidc.Hash(req.Code))
if err != nil { if err != nil {
if errors.Is(err, service.ErrCodeNotFound) { if errors.Is(err, service.ErrCodeNotFound) {
tlog.App.Warn().Str("code", req.Code).Msg("Code not found") tlog.App.Warn().Msg("Code not found")
c.JSON(400, gin.H{ c.JSON(400, gin.H{
"error": "invalid_grant", "error": "invalid_grant",
}) })
return return
} }
if errors.Is(err, service.ErrCodeExpired) { if errors.Is(err, service.ErrCodeExpired) {
tlog.App.Warn().Str("code", req.Code).Msg("Code expired") tlog.App.Warn().Msg("Code expired")
c.JSON(400, gin.H{ c.JSON(400, gin.H{
"error": "invalid_grant", "error": "invalid_grant",
}) })
@@ -273,7 +273,7 @@ func (controller *OIDCController) Token(c *gin.Context) {
tokenResponse = tokenRes tokenResponse = tokenRes
case "refresh_token": case "refresh_token":
tokenRes, err := controller.oidc.RefreshAccessToken(c, req.RefreshToken) tokenRes, err := controller.oidc.RefreshAccessToken(c, req.RefreshToken, rclientId)
if err != nil { if err != nil {
if errors.Is(err, service.ErrTokenExpired) { if errors.Is(err, service.ErrTokenExpired) {
@@ -284,6 +284,14 @@ func (controller *OIDCController) Token(c *gin.Context) {
return return
} }
if errors.Is(err, service.ErrInvalidClient) {
tlog.App.Error().Err(err).Msg("Invalid client")
c.JSON(401, gin.H{
"error": "invalid_grant",
})
return
}
tlog.App.Error().Err(err).Msg("Failed to refresh access token") tlog.App.Error().Err(err).Msg("Failed to refresh access token")
c.JSON(400, gin.H{ c.JSON(400, gin.H{
"error": "server_error", "error": "server_error",

View File

@@ -176,6 +176,8 @@ func TestOIDCController(t *testing.T) {
req, err = http.NewRequest("POST", "/api/oidc/token", strings.NewReader(params.Encode())) req, err = http.NewRequest("POST", "/api/oidc/token", strings.NewReader(params.Encode()))
assert.NilError(t, err)
req.Header.Set("content-type", "application/x-www-form-urlencoded") req.Header.Set("content-type", "application/x-www-form-urlencoded")
req.SetBasicAuth("some-client-id", "some-client-secret") req.SetBasicAuth("some-client-id", "some-client-secret")

View File

@@ -37,6 +37,7 @@ var (
ErrCodeNotFound = errors.New("code_not_found") ErrCodeNotFound = errors.New("code_not_found")
ErrTokenNotFound = errors.New("token_not_found") ErrTokenNotFound = errors.New("token_not_found")
ErrTokenExpired = errors.New("token_expired") ErrTokenExpired = errors.New("token_expired")
ErrInvalidClient = errors.New("invalid_client")
) )
type ClaimSet struct { type ClaimSet struct {
@@ -212,7 +213,7 @@ func (service *OIDCService) Init() error {
} }
func (service *OIDCService) GetIssuer() string { func (service *OIDCService) GetIssuer() string {
return service.config.Issuer return service.issuer
} }
func (service *OIDCService) GetClient(id string) (config.OIDCClientConfig, bool) { func (service *OIDCService) GetClient(id string) (config.OIDCClientConfig, bool) {
@@ -424,7 +425,7 @@ func (service *OIDCService) GenerateAccessToken(c *gin.Context, client config.OI
return tokenResponse, nil return tokenResponse, nil
} }
func (service *OIDCService) RefreshAccessToken(c *gin.Context, refreshToken string) (TokenResponse, error) { func (service *OIDCService) RefreshAccessToken(c *gin.Context, refreshToken string, reqClientId string) (TokenResponse, error) {
entry, err := service.queries.GetOidcTokenByRefreshToken(c, service.Hash(refreshToken)) entry, err := service.queries.GetOidcTokenByRefreshToken(c, service.Hash(refreshToken))
if err != nil { if err != nil {
@@ -438,6 +439,11 @@ func (service *OIDCService) RefreshAccessToken(c *gin.Context, refreshToken stri
return TokenResponse{}, ErrTokenExpired return TokenResponse{}, ErrTokenExpired
} }
// Ensure the client ID in the request matches the client ID in the token
if entry.ClientID != reqClientId {
return TokenResponse{}, ErrInvalidClient
}
idToken, err := service.generateIDToken(config.OIDCClientConfig{ idToken, err := service.generateIDToken(config.OIDCClientConfig{
ClientID: entry.ClientID, ClientID: entry.ClientID,
}, entry.Sub) }, entry.Sub)