mirror of
https://github.com/tinyauthapp/tinyauth.git
synced 2026-08-28 00:13:35 +08:00
Compare commits
4
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6fe21a7e38 | ||
|
|
0da39c93b0 | ||
|
|
766a7287e3 | ||
|
|
4bb766997b |
@@ -81,7 +81,6 @@ A huge thank you to [selfh.st](https://selfh.st) for their generous donation to
|
||||
|
||||
- [JetBrains for Open-Source](https://jb.gg/OpenSource)
|
||||
- [CodeRabbit AI](https://www.coderabbit.ai)
|
||||
- [Dosu](https://dosu.dev/)
|
||||
- [InstaPods](https://instapods.com) - [Deploy Tinyauth from 3$/month](https://app.instapods.com/dashboard/pods/create?app=tinyauth&ref=tinyauth)
|
||||
|
||||
## Acknowledgements
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
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 { useLocation } from "react-router";
|
||||
import {
|
||||
@@ -25,7 +25,8 @@ import {
|
||||
searchParamsFromObject,
|
||||
useScreenParams,
|
||||
} from "@/lib/hooks/screen-params";
|
||||
import { useEffect } from "react";
|
||||
import {useEffect, useState} from "react";
|
||||
import { z } from "zod";
|
||||
|
||||
type Scope = {
|
||||
id: string;
|
||||
@@ -34,6 +35,10 @@ type Scope = {
|
||||
icon: React.ReactNode;
|
||||
};
|
||||
|
||||
const skipConsentResponseSchema = z.object({
|
||||
skipConsent: z.boolean(),
|
||||
})
|
||||
|
||||
const scopeMapIconProps = {
|
||||
className: "stroke-muted-foreground stroke-[1.75] h-4",
|
||||
};
|
||||
@@ -96,14 +101,8 @@ export const AuthorizePage = () => {
|
||||
}
|
||||
return "";
|
||||
})();
|
||||
|
||||
// TODO: maybe a better way to do this
|
||||
const shouldAutoAuthorize =
|
||||
auth.authenticated &&
|
||||
isOidc &&
|
||||
screenParams.oidc_ticket !== undefined &&
|
||||
screenParams.oidc_scope !== undefined &&
|
||||
screenParams.oidc_prompt === "none";
|
||||
const [autoAuthorize, setAutoAuthorize] = useState(false);
|
||||
const [skipConsentChecked, setSkipConsentChecked] = useState(false);
|
||||
|
||||
const { mutate: authorizeMutate, isPending: authorizePending } = useMutation({
|
||||
mutationFn: () => {
|
||||
@@ -126,10 +125,34 @@ export const AuthorizePage = () => {
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (shouldAutoAuthorize) {
|
||||
authorizeMutate();
|
||||
}
|
||||
}, [shouldAutoAuthorize, authorizeMutate]);
|
||||
let active = true;
|
||||
const controller = new AbortController();
|
||||
|
||||
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) {
|
||||
return (
|
||||
@@ -190,13 +213,13 @@ export const AuthorizePage = () => {
|
||||
<CardFooter className="flex flex-col items-stretch gap-3">
|
||||
<Button
|
||||
onClick={() => authorizeMutate()}
|
||||
loading={authorizePending || shouldAutoAuthorize}
|
||||
loading={authorizePending || autoAuthorize || !skipConsentChecked}
|
||||
>
|
||||
{t("authorizeTitle")}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => navigate(`/logout${compiledParams}`)}
|
||||
disabled={authorizePending || shouldAutoAuthorize}
|
||||
disabled={authorizePending || autoAuthorize}
|
||||
variant="outline"
|
||||
>
|
||||
{t("cancelTitle")}
|
||||
|
||||
@@ -9,7 +9,7 @@ export default defineConfig({
|
||||
plugins: [react(), tailwindcss(), visualizer()],
|
||||
resolve: {
|
||||
alias: {
|
||||
"@": path.resolve(__dirname, "./src"),
|
||||
"@": path.resolve(import.meta.dirname, "./src"),
|
||||
},
|
||||
},
|
||||
build: {
|
||||
|
||||
@@ -18,3 +18,8 @@ type RedirectQuery struct {
|
||||
RedirectURI string `url:"redirect_uri"`
|
||||
LoginFor FrontendLoginFor `url:"login_for"`
|
||||
}
|
||||
|
||||
type SimpleResponse struct {
|
||||
Status int `json:"status"`
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
@@ -62,15 +62,19 @@ type ErrorScreen struct {
|
||||
Error string `url:"error"`
|
||||
}
|
||||
|
||||
type ClientRequest struct {
|
||||
ClientID string `uri:"id" binding:"required"`
|
||||
}
|
||||
|
||||
type ClientCredentials struct {
|
||||
ClientID string
|
||||
ClientSecret string
|
||||
}
|
||||
|
||||
type SkipConsentRequest struct {
|
||||
OIDCTicket string `form:"oidc_ticket" binding:"required"`
|
||||
}
|
||||
|
||||
type SkipConsentResponse struct {
|
||||
SkipConsent bool `json:"skipConsent"`
|
||||
}
|
||||
|
||||
type AuthorizeScreenParams struct {
|
||||
LoginFor FrontendLoginFor `url:"login_for"`
|
||||
OIDCTicket string `url:"oidc_ticket"`
|
||||
@@ -105,6 +109,7 @@ func NewOIDCController(i OIDCControllerInput) *OIDCController {
|
||||
|
||||
oidcGroup := i.RouterGroup.Group("/oidc")
|
||||
oidcGroup.POST("/authorize-complete", controller.authorizeComplete)
|
||||
oidcGroup.GET("/skip-consent", controller.skipConsent)
|
||||
oidcGroup.POST("/token", controller.Token)
|
||||
oidcGroup.GET("/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)
|
||||
|
||||
if err != nil {
|
||||
@@ -270,6 +265,90 @@ func (controller *OIDCController) authorize(c *gin.Context) {
|
||||
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.
|
||||
// It is called by the frontend after the user has logged in and given consent.
|
||||
func (controller *OIDCController) authorizeComplete(c *gin.Context) {
|
||||
@@ -330,6 +409,9 @@ func (controller *OIDCController) authorizeComplete(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
// We no longer need the ticket
|
||||
controller.oidc.DeleteAuthorizeRequestTicket(req.Ticket)
|
||||
|
||||
// Get the client
|
||||
client, ok := controller.oidc.GetClient(authorizeReq.ClientID)
|
||||
|
||||
@@ -343,9 +425,6 @@ func (controller *OIDCController) authorizeComplete(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
// We no longer need the ticket
|
||||
controller.oidc.DeleteAuthorizeRequestTicket(req.Ticket)
|
||||
|
||||
// Create the sub to find and delete old sessions
|
||||
sub := controller.oidc.CreateSub(*userContext, authorizeReq.ClientID)
|
||||
|
||||
|
||||
@@ -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},
|
||||
run: func(t *testing.T, router *gin.Engine, recorder *httptest.ResponseRecorder) {
|
||||
_, err := store.UpsertOIDCConsent(ctx, repository.UpsertOIDCConsentParams{
|
||||
@@ -189,77 +189,9 @@ func TestOIDCController(t *testing.T) {
|
||||
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 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)
|
||||
|
||||
// The consent skip check now happens after auth via the
|
||||
// skip-consent endpoint, so authorize should never set
|
||||
// prompt=none on its own.
|
||||
assert.Equal(t, http.StatusFound, recorder.Code)
|
||||
location := recorder.Header().Get("Location")
|
||||
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 ---
|
||||
{
|
||||
description: "Should fail if oidc is disabled",
|
||||
@@ -423,6 +581,60 @@ func TestOIDCController(t *testing.T) {
|
||||
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 ---
|
||||
{
|
||||
|
||||
@@ -53,6 +53,17 @@ const (
|
||||
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)}
|
||||
|
||||
// This is not spec-compliant, the ID token SHOULD NOT contain user info claims but,
|
||||
|
||||
Reference in New Issue
Block a user