mirror of
https://github.com/tinyauthapp/tinyauth.git
synced 2026-09-07 23:13:32 +08:00
fix: consent screen skip status should get checked after auth
This commit is contained in:
@@ -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,7 @@ export const AuthorizePage = () => {
|
|||||||
}
|
}
|
||||||
return "";
|
return "";
|
||||||
})();
|
})();
|
||||||
|
const [autoAuthorize, setAutoAuthorize] = useState(false);
|
||||||
// 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 { mutate: authorizeMutate, isPending: authorizePending } = useMutation({
|
const { mutate: authorizeMutate, isPending: authorizePending } = useMutation({
|
||||||
mutationFn: () => {
|
mutationFn: () => {
|
||||||
@@ -126,10 +124,21 @@ export const AuthorizePage = () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (shouldAutoAuthorize) {
|
const checkSkipConsent = async () => {
|
||||||
authorizeMutate();
|
try {
|
||||||
}
|
const res = await fetch(
|
||||||
}, [shouldAutoAuthorize, authorizeMutate]);
|
`/api/oidc/skip-consent?oidc_ticket=${encodeURIComponent(screenParams.oidc_ticket ?? "")}`,
|
||||||
|
);
|
||||||
|
if (!res.ok) return;
|
||||||
|
const parsed = skipConsentResponseSchema.safeParse(await res.json());
|
||||||
|
if (!parsed.success || !parsed.data.skipConsent) return;
|
||||||
|
setAutoAuthorize(true);
|
||||||
|
authorizeMutate();
|
||||||
|
} catch {}
|
||||||
|
};
|
||||||
|
|
||||||
|
checkSkipConsent();
|
||||||
|
}, []);
|
||||||
|
|
||||||
if (!isOidc || !screenParams.oidc_ticket || !screenParams.oidc_scope) {
|
if (!isOidc || !screenParams.oidc_ticket || !screenParams.oidc_scope) {
|
||||||
return (
|
return (
|
||||||
@@ -190,13 +199,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}
|
||||||
>
|
>
|
||||||
{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")}
|
||||||
|
|||||||
@@ -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"`
|
||||||
|
}
|
||||||
|
|||||||
@@ -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,87 @@ func (controller *OIDCController) authorize(c *gin.Context) {
|
|||||||
c.Redirect(http.StatusFound, redirectUrl)
|
c.Redirect(http.StatusFound, redirectUrl)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (controller *OIDCController) skipConsent(c *gin.Context) {
|
||||||
|
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.OIDCPromptNone.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 +406,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 +422,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)
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
Reference in New Issue
Block a user