Compare commits

..
Author SHA1 Message Date
Stavros 6fe21a7e38 fix: rabbit comments 2026-08-27 17:53:38 +03:00
Stavros 0da39c93b0 tests: add tests for skip consent 2026-08-27 17:22:15 +03:00
Stavros 766a7287e3 fix: consent screen skip status should get checked after auth 2026-08-27 17:08:08 +03:00
6 changed files with 436 additions and 106 deletions
+39 -16
View File
@@ -1,5 +1,5 @@
import { useUserContext } from "@/context/user-context"; import { useUserContext } from "@/context/user-context";
import { useMutation } from "@tanstack/react-query"; import {useMutation} from "@tanstack/react-query";
import { Navigate, useNavigate } from "react-router"; import { Navigate, useNavigate } from "react-router";
import { useLocation } from "react-router"; import { useLocation } from "react-router";
import { import {
@@ -25,7 +25,8 @@ import {
searchParamsFromObject, searchParamsFromObject,
useScreenParams, useScreenParams,
} from "@/lib/hooks/screen-params"; } from "@/lib/hooks/screen-params";
import { useEffect } from "react"; import {useEffect, useState} from "react";
import { z } from "zod";
type Scope = { type Scope = {
id: string; id: string;
@@ -34,6 +35,10 @@ type Scope = {
icon: React.ReactNode; icon: React.ReactNode;
}; };
const skipConsentResponseSchema = z.object({
skipConsent: z.boolean(),
})
const scopeMapIconProps = { const scopeMapIconProps = {
className: "stroke-muted-foreground stroke-[1.75] h-4", className: "stroke-muted-foreground stroke-[1.75] h-4",
}; };
@@ -96,14 +101,8 @@ export const AuthorizePage = () => {
} }
return ""; return "";
})(); })();
const [autoAuthorize, setAutoAuthorize] = useState(false);
// TODO: maybe a better way to do this const [skipConsentChecked, setSkipConsentChecked] = useState(false);
const shouldAutoAuthorize =
auth.authenticated &&
isOidc &&
screenParams.oidc_ticket !== undefined &&
screenParams.oidc_scope !== undefined &&
screenParams.oidc_prompt === "none";
const { mutate: authorizeMutate, isPending: authorizePending } = useMutation({ const { mutate: authorizeMutate, isPending: authorizePending } = useMutation({
mutationFn: () => { mutationFn: () => {
@@ -126,10 +125,34 @@ export const AuthorizePage = () => {
}); });
useEffect(() => { useEffect(() => {
if (shouldAutoAuthorize) { let active = true;
authorizeMutate(); const controller = new AbortController();
}
}, [shouldAutoAuthorize, authorizeMutate]); const checkSkipConsent = async () => {
try {
const res = await fetch(
`/api/oidc/skip-consent?oidc_ticket=${encodeURIComponent( screenParams.oidc_ticket ?? "")}`,
{ signal: controller.signal },
);
if (!res.ok) return;
const parsed = skipConsentResponseSchema.safeParse(await res.json());
if (!active || !parsed.success || !parsed.data.skipConsent) return;
setAutoAuthorize(true);
authorizeMutate();
} catch {
// Fall back to manual consent on any failure (including abort).
} finally {
if (active) setSkipConsentChecked(true);
}
};
checkSkipConsent();
return () => {
active = false;
controller.abort();
};
}, [authorizeMutate, screenParams.oidc_ticket]);
if (!isOidc || !screenParams.oidc_ticket || !screenParams.oidc_scope) { if (!isOidc || !screenParams.oidc_ticket || !screenParams.oidc_scope) {
return ( return (
@@ -190,13 +213,13 @@ export const AuthorizePage = () => {
<CardFooter className="flex flex-col items-stretch gap-3"> <CardFooter className="flex flex-col items-stretch gap-3">
<Button <Button
onClick={() => authorizeMutate()} onClick={() => authorizeMutate()}
loading={authorizePending || shouldAutoAuthorize} loading={authorizePending || autoAuthorize || !skipConsentChecked}
> >
{t("authorizeTitle")} {t("authorizeTitle")}
</Button> </Button>
<Button <Button
onClick={() => navigate(`/logout${compiledParams}`)} onClick={() => navigate(`/logout${compiledParams}`)}
disabled={authorizePending || shouldAutoAuthorize} disabled={authorizePending || autoAuthorize}
variant="outline" variant="outline"
> >
{t("cancelTitle")} {t("cancelTitle")}
+1 -1
View File
@@ -9,7 +9,7 @@ export default defineConfig({
plugins: [react(), tailwindcss(), visualizer()], plugins: [react(), tailwindcss(), visualizer()],
resolve: { resolve: {
alias: { alias: {
"@": path.resolve(__dirname, "./src"), "@": path.resolve(import.meta.dirname, "./src"),
}, },
}, },
build: { build: {
+5
View File
@@ -18,3 +18,8 @@ type RedirectQuery struct {
RedirectURI string `url:"redirect_uri"` RedirectURI string `url:"redirect_uri"`
LoginFor FrontendLoginFor `url:"login_for"` LoginFor FrontendLoginFor `url:"login_for"`
} }
type SimpleResponse struct {
Status int `json:"status"`
Message string `json:"message"`
}
+96 -17
View File
@@ -62,15 +62,19 @@ type ErrorScreen struct {
Error string `url:"error"` Error string `url:"error"`
} }
type ClientRequest struct {
ClientID string `uri:"id" binding:"required"`
}
type ClientCredentials struct { type ClientCredentials struct {
ClientID string ClientID string
ClientSecret string ClientSecret string
} }
type SkipConsentRequest struct {
OIDCTicket string `form:"oidc_ticket" binding:"required"`
}
type SkipConsentResponse struct {
SkipConsent bool `json:"skipConsent"`
}
type AuthorizeScreenParams struct { type AuthorizeScreenParams struct {
LoginFor FrontendLoginFor `url:"login_for"` LoginFor FrontendLoginFor `url:"login_for"`
OIDCTicket string `url:"oidc_ticket"` OIDCTicket string `url:"oidc_ticket"`
@@ -105,6 +109,7 @@ func NewOIDCController(i OIDCControllerInput) *OIDCController {
oidcGroup := i.RouterGroup.Group("/oidc") oidcGroup := i.RouterGroup.Group("/oidc")
oidcGroup.POST("/authorize-complete", controller.authorizeComplete) oidcGroup.POST("/authorize-complete", controller.authorizeComplete)
oidcGroup.GET("/skip-consent", controller.skipConsent)
oidcGroup.POST("/token", controller.Token) oidcGroup.POST("/token", controller.Token)
oidcGroup.GET("/userinfo", controller.Userinfo) oidcGroup.GET("/userinfo", controller.Userinfo)
oidcGroup.POST("/userinfo", controller.Userinfo) oidcGroup.POST("/userinfo", controller.Userinfo)
@@ -242,16 +247,6 @@ func (controller *OIDCController) authorize(c *gin.Context) {
} }
} }
if userContext != nil && userContext.Authenticated && values.OIDCPrompt != service.OIDCPromptLogin {
consent, err := controller.oidc.GetOIDCConsent(c, userContext.GetUsername(), req.ClientID)
if err != nil {
controller.log.App.Warn().Err(err).Msg("Failed to get OIDC consent")
} else if consent != nil && scopesGranted(consent.Scope, req.Scope) {
values.OIDCPrompt = service.OIDCPromptNone
}
}
queries, err := query.Values(values) queries, err := query.Values(values)
if err != nil { if err != nil {
@@ -270,6 +265,90 @@ func (controller *OIDCController) authorize(c *gin.Context) {
c.Redirect(http.StatusFound, redirectUrl) c.Redirect(http.StatusFound, redirectUrl)
} }
func (controller *OIDCController) skipConsent(c *gin.Context) {
c.Header("cache-control", "no-store")
c.Header("pragma", "no-cache")
if controller.oidc == nil {
c.JSON(500, SimpleResponse{
Status: 500,
Message: "OIDC not configured",
})
return
}
userContext, err := new(model.UserContext).NewFromGin(c)
if err != nil {
if !errors.Is(err, model.ErrUserContextNotFound) {
controller.log.App.Warn().Err(err).Msg("Failed to get user context")
}
}
if err != nil || !userContext.Authenticated {
c.JSON(401, SimpleResponse{
Status: 401,
Message: "User not logged in",
})
return
}
var req SkipConsentRequest
err = c.BindQuery(&req)
if err != nil {
c.JSON(400, SimpleResponse{
Status: 400,
Message: "Bad request",
})
return
}
controller.log.App.Debug().Interface("req", req).Msg("Received skip consent request")
authorizeReq, ok := controller.oidc.GetAuthorizeRequestByTicket(req.OIDCTicket)
if !ok {
c.JSON(200, SkipConsentResponse{
SkipConsent: false,
})
return
}
controller.log.App.Debug().Str("client", authorizeReq.ClientID).Str("user", userContext.GetUsername()).Msg("User consented to OIDC")
if authorizeReq.Prompt == service.OIDCPromptLogin.String() {
c.JSON(200, SkipConsentResponse{
SkipConsent: false,
})
return
}
consent, err := controller.oidc.GetOIDCConsent(c, userContext.GetUsername(), authorizeReq.ClientID)
if err != nil || consent == nil {
if err != nil {
controller.log.App.Warn().Err(err).Msg("Failed to get OIDC consent")
}
c.JSON(200, SkipConsentResponse{
SkipConsent: false,
})
return
}
if !scopesGranted(consent.Scope, authorizeReq.Scope) {
c.JSON(200, SkipConsentResponse{
SkipConsent: false,
})
return
}
c.JSON(200, SkipConsentResponse{
SkipConsent: true,
})
}
// The actual **internal** endpoint that actually creates the code and session. // The actual **internal** endpoint that actually creates the code and session.
// It is called by the frontend after the user has logged in and given consent. // It is called by the frontend after the user has logged in and given consent.
func (controller *OIDCController) authorizeComplete(c *gin.Context) { func (controller *OIDCController) authorizeComplete(c *gin.Context) {
@@ -330,6 +409,9 @@ func (controller *OIDCController) authorizeComplete(c *gin.Context) {
return return
} }
// We no longer need the ticket
controller.oidc.DeleteAuthorizeRequestTicket(req.Ticket)
// Get the client // Get the client
client, ok := controller.oidc.GetClient(authorizeReq.ClientID) client, ok := controller.oidc.GetClient(authorizeReq.ClientID)
@@ -343,9 +425,6 @@ func (controller *OIDCController) authorizeComplete(c *gin.Context) {
return return
} }
// We no longer need the ticket
controller.oidc.DeleteAuthorizeRequestTicket(req.Ticket)
// Create the sub to find and delete old sessions // Create the sub to find and delete old sessions
sub := controller.oidc.CreateSub(*userContext, authorizeReq.ClientID) sub := controller.oidc.CreateSub(*userContext, authorizeReq.ClientID)
+284 -72
View File
@@ -171,7 +171,7 @@ func TestOIDCController(t *testing.T) {
}, },
}, },
{ {
description: "Authorize skips the consent screen when all requested scopes were already granted", description: "Authorize does not skip the consent screen even when consent was already granted",
middlewares: []gin.HandlerFunc{authedUser}, middlewares: []gin.HandlerFunc{authedUser},
run: func(t *testing.T, router *gin.Engine, recorder *httptest.ResponseRecorder) { run: func(t *testing.T, router *gin.Engine, recorder *httptest.ResponseRecorder) {
_, err := store.UpsertOIDCConsent(ctx, repository.UpsertOIDCConsentParams{ _, err := store.UpsertOIDCConsent(ctx, repository.UpsertOIDCConsentParams{
@@ -189,77 +189,9 @@ func TestOIDCController(t *testing.T) {
req := httptest.NewRequest("GET", "/authorize?"+q.Encode(), nil) req := httptest.NewRequest("GET", "/authorize?"+q.Encode(), nil)
router.ServeHTTP(recorder, req) router.ServeHTTP(recorder, req)
assert.Equal(t, http.StatusFound, recorder.Code) // The consent skip check now happens after auth via the
location := recorder.Header().Get("Location") // skip-consent endpoint, so authorize should never set
assert.True(t, strings.HasPrefix(location, oidcService.GetIssuer()+"/oidc/authorize?")) // prompt=none on its own.
assert.Contains(t, location, "oidc_prompt=none")
},
},
{
description: "Authorize shows the consent screen when a new scope is requested",
middlewares: []gin.HandlerFunc{authedUser},
run: func(t *testing.T, router *gin.Engine, recorder *httptest.ResponseRecorder) {
_, err := store.UpsertOIDCConsent(ctx, repository.UpsertOIDCConsentParams{
Username: "testuser", ClientID: "some-client-id",
Scope: "openid", CreatedAt: time.Now().Unix(),
})
require.NoError(t, err)
q := url.Values{}
q.Set("scope", "openid profile")
q.Set("response_type", "code")
q.Set("client_id", "some-client-id")
q.Set("redirect_uri", "https://test.example.com/callback")
req := httptest.NewRequest("GET", "/authorize?"+q.Encode(), nil)
router.ServeHTTP(recorder, req)
assert.Equal(t, http.StatusFound, recorder.Code)
location := recorder.Header().Get("Location")
assert.True(t, strings.HasPrefix(location, oidcService.GetIssuer()+"/oidc/authorize?"))
assert.NotContains(t, location, "oidc_prompt=none")
},
},
{
description: "Authorize skips the consent screen for a subset of already granted scopes",
middlewares: []gin.HandlerFunc{authedUser},
run: func(t *testing.T, router *gin.Engine, recorder *httptest.ResponseRecorder) {
_, err := store.UpsertOIDCConsent(ctx, repository.UpsertOIDCConsentParams{
Username: "testuser", ClientID: "some-client-id",
Scope: "openid profile email", CreatedAt: time.Now().Unix(),
})
require.NoError(t, err)
q := url.Values{}
q.Set("scope", "openid profile")
q.Set("response_type", "code")
q.Set("client_id", "some-client-id")
q.Set("redirect_uri", "https://test.example.com/callback")
req := httptest.NewRequest("GET", "/authorize?"+q.Encode(), nil)
router.ServeHTTP(recorder, req)
assert.Equal(t, http.StatusFound, recorder.Code)
location := recorder.Header().Get("Location")
assert.True(t, strings.HasPrefix(location, oidcService.GetIssuer()+"/oidc/authorize?"))
assert.Contains(t, location, "oidc_prompt=none")
},
},
{
description: "Authorize shows the consent screen when no consent was granted yet",
middlewares: []gin.HandlerFunc{authedUser},
run: func(t *testing.T, router *gin.Engine, recorder *httptest.ResponseRecorder) {
require.NoError(t, store.DeleteOIDCConsentByClientID(ctx, "some-client-id"))
q := url.Values{}
q.Set("scope", "openid profile")
q.Set("response_type", "code")
q.Set("client_id", "some-client-id")
q.Set("redirect_uri", "https://test.example.com/callback")
req := httptest.NewRequest("GET", "/authorize?"+q.Encode(), nil)
router.ServeHTTP(recorder, req)
assert.Equal(t, http.StatusFound, recorder.Code) assert.Equal(t, http.StatusFound, recorder.Code)
location := recorder.Header().Get("Location") location := recorder.Header().Get("Location")
assert.True(t, strings.HasPrefix(location, oidcService.GetIssuer()+"/oidc/authorize?")) assert.True(t, strings.HasPrefix(location, oidcService.GetIssuer()+"/oidc/authorize?"))
@@ -304,6 +236,232 @@ func TestOIDCController(t *testing.T) {
}, },
}, },
// --- skip-consent ---
{
description: "Skip consent returns 500 when OIDC is not configured",
oidcDisabled: true,
run: func(t *testing.T, router *gin.Engine, recorder *httptest.ResponseRecorder) {
req := httptest.NewRequest("GET", "/api/oidc/skip-consent?oidc_ticket=some-ticket", nil)
router.ServeHTTP(recorder, req)
assert.Equal(t, http.StatusInternalServerError, recorder.Code)
},
},
{
description: "Skip consent returns 401 when the user context is missing",
run: func(t *testing.T, router *gin.Engine, recorder *httptest.ResponseRecorder) {
req := httptest.NewRequest("GET", "/api/oidc/skip-consent?oidc_ticket=some-ticket", nil)
router.ServeHTTP(recorder, req)
assert.Equal(t, http.StatusUnauthorized, recorder.Code)
},
},
{
description: "Skip consent returns 401 when the user is not authenticated",
middlewares: []gin.HandlerFunc{
func(c *gin.Context) {
c.Set("context", &model.UserContext{
Authenticated: false,
Provider: model.ProviderLocal,
Local: &model.LocalContext{
BaseContext: model.BaseContext{Username: "testuser"},
},
})
},
},
run: func(t *testing.T, router *gin.Engine, recorder *httptest.ResponseRecorder) {
req := httptest.NewRequest("GET", "/api/oidc/skip-consent?oidc_ticket=some-ticket", nil)
router.ServeHTTP(recorder, req)
assert.Equal(t, http.StatusUnauthorized, recorder.Code)
},
},
{
description: "Skip consent returns 400 when the ticket is missing",
middlewares: []gin.HandlerFunc{authedUser},
run: func(t *testing.T, router *gin.Engine, recorder *httptest.ResponseRecorder) {
req := httptest.NewRequest("GET", "/api/oidc/skip-consent", nil)
router.ServeHTTP(recorder, req)
assert.Equal(t, http.StatusBadRequest, recorder.Code)
},
},
{
description: "Skip consent returns false when the ticket is unknown",
middlewares: []gin.HandlerFunc{authedUser},
run: func(t *testing.T, router *gin.Engine, recorder *httptest.ResponseRecorder) {
req := httptest.NewRequest("GET", "/api/oidc/skip-consent?oidc_ticket=nonexistent-ticket", nil)
router.ServeHTTP(recorder, req)
assert.Equal(t, http.StatusOK, recorder.Code)
var res SkipConsentResponse
require.NoError(t, json.Unmarshal(recorder.Body.Bytes(), &res))
assert.False(t, res.SkipConsent)
},
},
{
description: "Skip consent returns true when the authorize request has prompt none",
middlewares: []gin.HandlerFunc{authedUser},
run: func(t *testing.T, router *gin.Engine, recorder *httptest.ResponseRecorder) {
_, err := store.UpsertOIDCConsent(ctx, repository.UpsertOIDCConsentParams{
Username: "testuser", ClientID: "some-client-id",
Scope: "openid profile", CreatedAt: time.Now().Unix(),
})
require.NoError(t, err)
ticket := oidcService.CreateAuthorizeRequestTicket(service.AuthorizeRequest{
Scope: "openid profile",
ResponseType: "code",
ClientID: "some-client-id",
RedirectURI: "https://test.example.com/callback",
Prompt: "none",
})
req := httptest.NewRequest("GET", "/api/oidc/skip-consent?oidc_ticket="+url.QueryEscape(ticket), nil)
router.ServeHTTP(recorder, req)
assert.Equal(t, http.StatusOK, recorder.Code)
var res SkipConsentResponse
require.NoError(t, json.Unmarshal(recorder.Body.Bytes(), &res))
assert.True(t, res.SkipConsent)
},
},
{
description: "Skip consent returns false when no consent was granted yet",
middlewares: []gin.HandlerFunc{authedUser},
run: func(t *testing.T, router *gin.Engine, recorder *httptest.ResponseRecorder) {
require.NoError(t, store.DeleteOIDCConsentByClientID(ctx, "some-client-id"))
ticket := oidcService.CreateAuthorizeRequestTicket(service.AuthorizeRequest{
Scope: "openid profile",
ResponseType: "code",
ClientID: "some-client-id",
RedirectURI: "https://test.example.com/callback",
})
req := httptest.NewRequest("GET", "/api/oidc/skip-consent?oidc_ticket="+url.QueryEscape(ticket), nil)
router.ServeHTTP(recorder, req)
assert.Equal(t, http.StatusOK, recorder.Code)
var res SkipConsentResponse
require.NoError(t, json.Unmarshal(recorder.Body.Bytes(), &res))
assert.False(t, res.SkipConsent)
},
},
{
description: "Skip consent returns false when a new scope is requested",
middlewares: []gin.HandlerFunc{authedUser},
run: func(t *testing.T, router *gin.Engine, recorder *httptest.ResponseRecorder) {
_, err := store.UpsertOIDCConsent(ctx, repository.UpsertOIDCConsentParams{
Username: "testuser", ClientID: "some-client-id",
Scope: "openid", CreatedAt: time.Now().Unix(),
})
require.NoError(t, err)
ticket := oidcService.CreateAuthorizeRequestTicket(service.AuthorizeRequest{
Scope: "openid profile",
ResponseType: "code",
ClientID: "some-client-id",
RedirectURI: "https://test.example.com/callback",
})
req := httptest.NewRequest("GET", "/api/oidc/skip-consent?oidc_ticket="+url.QueryEscape(ticket), nil)
router.ServeHTTP(recorder, req)
assert.Equal(t, http.StatusOK, recorder.Code)
var res SkipConsentResponse
require.NoError(t, json.Unmarshal(recorder.Body.Bytes(), &res))
assert.False(t, res.SkipConsent)
},
},
{
description: "Skip consent returns true when all requested scopes were already granted",
middlewares: []gin.HandlerFunc{authedUser},
run: func(t *testing.T, router *gin.Engine, recorder *httptest.ResponseRecorder) {
_, err := store.UpsertOIDCConsent(ctx, repository.UpsertOIDCConsentParams{
Username: "testuser", ClientID: "some-client-id",
Scope: "openid profile", CreatedAt: time.Now().Unix(),
})
require.NoError(t, err)
ticket := oidcService.CreateAuthorizeRequestTicket(service.AuthorizeRequest{
Scope: "openid profile",
ResponseType: "code",
ClientID: "some-client-id",
RedirectURI: "https://test.example.com/callback",
})
req := httptest.NewRequest("GET", "/api/oidc/skip-consent?oidc_ticket="+url.QueryEscape(ticket), nil)
router.ServeHTTP(recorder, req)
assert.Equal(t, http.StatusOK, recorder.Code)
var res SkipConsentResponse
require.NoError(t, json.Unmarshal(recorder.Body.Bytes(), &res))
assert.True(t, res.SkipConsent)
},
},
{
description: "Skip consent returns true for a subset of already granted scopes",
middlewares: []gin.HandlerFunc{authedUser},
run: func(t *testing.T, router *gin.Engine, recorder *httptest.ResponseRecorder) {
_, err := store.UpsertOIDCConsent(ctx, repository.UpsertOIDCConsentParams{
Username: "testuser", ClientID: "some-client-id",
Scope: "openid profile email", CreatedAt: time.Now().Unix(),
})
require.NoError(t, err)
ticket := oidcService.CreateAuthorizeRequestTicket(service.AuthorizeRequest{
Scope: "openid profile",
ResponseType: "code",
ClientID: "some-client-id",
RedirectURI: "https://test.example.com/callback",
})
req := httptest.NewRequest("GET", "/api/oidc/skip-consent?oidc_ticket="+url.QueryEscape(ticket), nil)
router.ServeHTTP(recorder, req)
assert.Equal(t, http.StatusOK, recorder.Code)
var res SkipConsentResponse
require.NoError(t, json.Unmarshal(recorder.Body.Bytes(), &res))
assert.True(t, res.SkipConsent)
},
},
{
description: "Skip consent returns false when the consent belongs to another user",
middlewares: []gin.HandlerFunc{authedUser},
run: func(t *testing.T, router *gin.Engine, recorder *httptest.ResponseRecorder) {
require.NoError(t, store.DeleteOIDCConsentByClientID(ctx, "some-client-id"))
_, err := store.UpsertOIDCConsent(ctx, repository.UpsertOIDCConsentParams{
Username: "otheruser", ClientID: "some-client-id",
Scope: "openid profile", CreatedAt: time.Now().Unix(),
})
require.NoError(t, err)
ticket := oidcService.CreateAuthorizeRequestTicket(service.AuthorizeRequest{
Scope: "openid profile",
ResponseType: "code",
ClientID: "some-client-id",
RedirectURI: "https://test.example.com/callback",
})
req := httptest.NewRequest("GET", "/api/oidc/skip-consent?oidc_ticket="+url.QueryEscape(ticket), nil)
router.ServeHTTP(recorder, req)
assert.Equal(t, http.StatusOK, recorder.Code)
var res SkipConsentResponse
require.NoError(t, json.Unmarshal(recorder.Body.Bytes(), &res))
assert.False(t, res.SkipConsent)
},
},
// --- authorize-complete --- // --- authorize-complete ---
{ {
description: "Should fail if oidc is disabled", description: "Should fail if oidc is disabled",
@@ -423,6 +581,60 @@ func TestOIDCController(t *testing.T) {
assert.Contains(t, redirectURI, "state=state-123") assert.Contains(t, redirectURI, "state=state-123")
}, },
}, },
{
description: "Authorize complete deletes the ticket on success",
middlewares: []gin.HandlerFunc{authedUser},
run: func(t *testing.T, router *gin.Engine, recorder *httptest.ResponseRecorder) {
ticket := oidcService.CreateAuthorizeRequestTicket(service.AuthorizeRequest{
Scope: "openid",
ResponseType: "code",
ClientID: "some-client-id",
RedirectURI: "https://test.example.com/callback",
})
body, err := json.Marshal(AuthorizeCompleteRequest{Ticket: ticket})
require.NoError(t, err)
req := httptest.NewRequest("POST", "/api/oidc/authorize-complete", strings.NewReader(string(body)))
req.Header.Set("Content-Type", "application/json")
router.ServeHTTP(recorder, req)
assert.Equal(t, http.StatusOK, recorder.Code)
_, ok := oidcService.GetAuthorizeRequestByTicket(ticket)
assert.False(t, ok)
},
},
{
description: "Authorize complete deletes the ticket even when the client is unknown",
middlewares: []gin.HandlerFunc{authedUser},
run: func(t *testing.T, router *gin.Engine, recorder *httptest.ResponseRecorder) {
ticket := oidcService.CreateAuthorizeRequestTicket(service.AuthorizeRequest{
Scope: "openid",
ResponseType: "code",
ClientID: "unknown-client",
RedirectURI: "https://test.example.com/callback",
})
body, err := json.Marshal(AuthorizeCompleteRequest{Ticket: ticket})
require.NoError(t, err)
req := httptest.NewRequest("POST", "/api/oidc/authorize-complete", strings.NewReader(string(body)))
req.Header.Set("Content-Type", "application/json")
router.ServeHTTP(recorder, req)
assert.Equal(t, http.StatusOK, recorder.Code)
var res map[string]any
require.NoError(t, json.Unmarshal(recorder.Body.Bytes(), &res))
redirectURI, ok := res["redirect_uri"].(string)
require.True(t, ok)
assert.Contains(t, redirectURI, oidcService.GetIssuer()+"/error")
_, ok = oidcService.GetAuthorizeRequestByTicket(ticket)
assert.False(t, ok)
},
},
// --- token --- // --- token ---
{ {
+11
View File
@@ -53,6 +53,17 @@ const (
OIDCPromptNone OIDCPrompt = "none" OIDCPromptNone OIDCPrompt = "none"
) )
func (p OIDCPrompt) String() string {
switch p {
case OIDCPromptLogin:
return "login"
case OIDCPromptNone:
return "none"
default:
return "login"
}
}
var SupportedPrompts = []string{string(OIDCPromptLogin), string(OIDCPromptNone)} var SupportedPrompts = []string{string(OIDCPromptLogin), string(OIDCPromptNone)}
// This is not spec-compliant, the ID token SHOULD NOT contain user info claims but, // This is not spec-compliant, the ID token SHOULD NOT contain user info claims but,