mirror of
https://github.com/steveiliop56/tinyauth.git
synced 2026-04-08 23:07:55 +00:00
Compare commits
1 Commits
refactor/o
...
docs/updat
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ff1a894133 |
@@ -1,40 +1,55 @@
|
||||
import { z } from "zod";
|
||||
export type OIDCValues = {
|
||||
scope: string;
|
||||
response_type: string;
|
||||
client_id: string;
|
||||
redirect_uri: string;
|
||||
state: string;
|
||||
nonce: string;
|
||||
};
|
||||
|
||||
export const oidcParamsSchema = z.object({
|
||||
scope: z.string().nonempty(),
|
||||
response_type: z.string().nonempty(),
|
||||
client_id: z.string().nonempty(),
|
||||
redirect_uri: z.string().nonempty(),
|
||||
state: z.string().optional(),
|
||||
nonce: z.string().optional(),
|
||||
code_challenge: z.string().optional(),
|
||||
code_challenge_method: z.string().optional(),
|
||||
});
|
||||
|
||||
export const useOIDCParams = (
|
||||
params: URLSearchParams,
|
||||
): {
|
||||
values: z.infer<typeof oidcParamsSchema>;
|
||||
issues: string[];
|
||||
isOidc: boolean;
|
||||
interface IuseOIDCParams {
|
||||
values: OIDCValues;
|
||||
compiled: string;
|
||||
} => {
|
||||
const obj = Object.fromEntries(params.entries());
|
||||
const parsed = oidcParamsSchema.safeParse(obj);
|
||||
isOidc: boolean;
|
||||
missingParams: string[];
|
||||
}
|
||||
|
||||
if (parsed.success) {
|
||||
return {
|
||||
values: parsed.data,
|
||||
issues: [],
|
||||
isOidc: true,
|
||||
compiled: new URLSearchParams(parsed.data).toString(),
|
||||
};
|
||||
const optionalParams: string[] = ["state", "nonce"];
|
||||
|
||||
export function useOIDCParams(params: URLSearchParams): IuseOIDCParams {
|
||||
let compiled: string = "";
|
||||
let isOidc = false;
|
||||
const missingParams: string[] = [];
|
||||
|
||||
const values: OIDCValues = {
|
||||
scope: params.get("scope") ?? "",
|
||||
response_type: params.get("response_type") ?? "",
|
||||
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)) {
|
||||
if (!values[key as keyof OIDCValues]) {
|
||||
if (!optionalParams.includes(key)) {
|
||||
missingParams.push(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (missingParams.length === 0) {
|
||||
isOidc = true;
|
||||
}
|
||||
|
||||
if (isOidc) {
|
||||
compiled = new URLSearchParams(values).toString();
|
||||
}
|
||||
|
||||
return {
|
||||
issues: parsed.error.issues.map((issue) => issue.path.toString()),
|
||||
values: {} as z.infer<typeof oidcParamsSchema>,
|
||||
isOidc: false,
|
||||
compiled: "",
|
||||
values,
|
||||
compiled,
|
||||
isOidc,
|
||||
missingParams,
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
@@ -72,27 +72,36 @@ export const AuthorizePage = () => {
|
||||
const scopeMap = createScopeMap(t);
|
||||
|
||||
const searchParams = new URLSearchParams(search);
|
||||
const oidcParams = useOIDCParams(searchParams);
|
||||
const {
|
||||
values: props,
|
||||
missingParams,
|
||||
isOidc,
|
||||
compiled: compiledOIDCParams,
|
||||
} = useOIDCParams(searchParams);
|
||||
const scopes = props.scope ? props.scope.split(" ").filter(Boolean) : [];
|
||||
|
||||
const getClientInfo = useQuery({
|
||||
queryKey: ["client", oidcParams.values.client_id],
|
||||
queryKey: ["client", props.client_id],
|
||||
queryFn: async () => {
|
||||
const res = await fetch(
|
||||
`/api/oidc/clients/${encodeURIComponent(oidcParams.values.client_id)}`,
|
||||
);
|
||||
const res = await fetch(`/api/oidc/clients/${props.client_id}`);
|
||||
const data = await getOidcClientInfoSchema.parseAsync(await res.json());
|
||||
return data;
|
||||
},
|
||||
enabled: oidcParams.isOidc,
|
||||
enabled: isOidc,
|
||||
});
|
||||
|
||||
const authorizeMutation = useMutation({
|
||||
mutationFn: () => {
|
||||
return axios.post("/api/oidc/authorize", {
|
||||
...oidcParams.values,
|
||||
scope: props.scope,
|
||||
response_type: props.response_type,
|
||||
client_id: props.client_id,
|
||||
redirect_uri: props.redirect_uri,
|
||||
state: props.state,
|
||||
nonce: props.nonce,
|
||||
});
|
||||
},
|
||||
mutationKey: ["authorize", oidcParams.values.client_id],
|
||||
mutationKey: ["authorize", props.client_id],
|
||||
onSuccess: (data) => {
|
||||
toast.info(t("authorizeSuccessTitle"), {
|
||||
description: t("authorizeSuccessSubtitle"),
|
||||
@@ -106,17 +115,17 @@ export const AuthorizePage = () => {
|
||||
},
|
||||
});
|
||||
|
||||
if (oidcParams.issues.length > 0) {
|
||||
if (missingParams.length > 0) {
|
||||
return (
|
||||
<Navigate
|
||||
to={`/error?error=${encodeURIComponent(t("authorizeErrorMissingParams", { missingParams: oidcParams.issues.join(", ") }))}`}
|
||||
to={`/error?error=${encodeURIComponent(t("authorizeErrorMissingParams", { missingParams: missingParams.join(", ") }))}`}
|
||||
replace
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (!isLoggedIn) {
|
||||
return <Navigate to={`/login?${oidcParams.compiled}`} replace />;
|
||||
return <Navigate to={`/login?${compiledOIDCParams}`} replace />;
|
||||
}
|
||||
|
||||
if (getClientInfo.isLoading) {
|
||||
@@ -143,9 +152,6 @@ export const AuthorizePage = () => {
|
||||
);
|
||||
}
|
||||
|
||||
const scopes =
|
||||
oidcParams.values.scope.split(" ").filter((s) => s.trim() !== "") || [];
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader className="mb-2">
|
||||
|
||||
@@ -51,12 +51,15 @@ export const LoginPage = () => {
|
||||
const formId = useId();
|
||||
|
||||
const searchParams = new URLSearchParams(search);
|
||||
const redirectUri = searchParams.get("redirect_uri") || undefined;
|
||||
const oidcParams = useOIDCParams(searchParams);
|
||||
const {
|
||||
values: props,
|
||||
isOidc,
|
||||
compiled: compiledOIDCParams,
|
||||
} = useOIDCParams(searchParams);
|
||||
|
||||
const [isOauthAutoRedirect, setIsOauthAutoRedirect] = useState(
|
||||
providers.find((provider) => provider.id === oauthAutoRedirect) !==
|
||||
undefined && redirectUri !== undefined,
|
||||
undefined && props.redirect_uri,
|
||||
);
|
||||
|
||||
const oauthProviders = providers.filter(
|
||||
@@ -75,7 +78,7 @@ export const LoginPage = () => {
|
||||
} = useMutation({
|
||||
mutationFn: (provider: string) =>
|
||||
axios.get(
|
||||
`/api/oauth/url/${provider}${redirectUri ? `?redirect_uri=${encodeURIComponent(redirectUri)}` : ""}`,
|
||||
`/api/oauth/url/${provider}${props.redirect_uri ? `?redirect_uri=${encodeURIComponent(props.redirect_uri)}` : ""}`,
|
||||
),
|
||||
mutationKey: ["oauth"],
|
||||
onSuccess: (data) => {
|
||||
@@ -106,12 +109,8 @@ export const LoginPage = () => {
|
||||
mutationKey: ["login"],
|
||||
onSuccess: (data) => {
|
||||
if (data.data.totpPending) {
|
||||
if (oidcParams.isOidc) {
|
||||
window.location.replace(`/totp?${oidcParams.compiled}`);
|
||||
return;
|
||||
}
|
||||
window.location.replace(
|
||||
`/totp${redirectUri ? `?redirect_uri=${encodeURIComponent(redirectUri)}` : ""}`,
|
||||
`/totp${props.redirect_uri ? `?redirect_uri=${encodeURIComponent(props.redirect_uri)}` : ""}`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
@@ -121,12 +120,12 @@ export const LoginPage = () => {
|
||||
});
|
||||
|
||||
redirectTimer.current = window.setTimeout(() => {
|
||||
if (oidcParams.isOidc) {
|
||||
window.location.replace(`/authorize?${oidcParams.compiled}`);
|
||||
if (isOidc) {
|
||||
window.location.replace(`/authorize?${compiledOIDCParams}`);
|
||||
return;
|
||||
}
|
||||
window.location.replace(
|
||||
`/continue${redirectUri ? `?redirect_uri=${encodeURIComponent(redirectUri)}` : ""}`,
|
||||
`/continue${props.redirect_uri ? `?redirect_uri=${encodeURIComponent(props.redirect_uri)}` : ""}`,
|
||||
);
|
||||
}, 500);
|
||||
},
|
||||
@@ -145,7 +144,7 @@ export const LoginPage = () => {
|
||||
!isLoggedIn &&
|
||||
isOauthAutoRedirect &&
|
||||
!hasAutoRedirectedRef.current &&
|
||||
redirectUri !== undefined
|
||||
props.redirect_uri
|
||||
) {
|
||||
hasAutoRedirectedRef.current = true;
|
||||
oauthMutate(oauthAutoRedirect);
|
||||
@@ -156,7 +155,7 @@ export const LoginPage = () => {
|
||||
hasAutoRedirectedRef,
|
||||
oauthAutoRedirect,
|
||||
isOauthAutoRedirect,
|
||||
redirectUri,
|
||||
props.redirect_uri,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -171,14 +170,14 @@ export const LoginPage = () => {
|
||||
};
|
||||
}, [redirectTimer, redirectButtonTimer]);
|
||||
|
||||
if (isLoggedIn && oidcParams.isOidc) {
|
||||
return <Navigate to={`/authorize?${oidcParams.compiled}`} replace />;
|
||||
if (isLoggedIn && isOidc) {
|
||||
return <Navigate to={`/authorize?${compiledOIDCParams}`} replace />;
|
||||
}
|
||||
|
||||
if (isLoggedIn && redirectUri !== undefined) {
|
||||
if (isLoggedIn && props.redirect_uri !== "") {
|
||||
return (
|
||||
<Navigate
|
||||
to={`/continue${redirectUri ? `?redirect_uri=${encodeURIComponent(redirectUri)}` : ""}`}
|
||||
to={`/continue${props.redirect_uri ? `?redirect_uri=${encodeURIComponent(props.redirect_uri)}` : ""}`}
|
||||
replace
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -27,8 +27,11 @@ export const TotpPage = () => {
|
||||
const redirectTimer = useRef<number | null>(null);
|
||||
|
||||
const searchParams = new URLSearchParams(search);
|
||||
const redirectUri = searchParams.get("redirect_uri") || undefined;
|
||||
const oidcParams = useOIDCParams(searchParams);
|
||||
const {
|
||||
values: props,
|
||||
isOidc,
|
||||
compiled: compiledOIDCParams,
|
||||
} = useOIDCParams(searchParams);
|
||||
|
||||
const totpMutation = useMutation({
|
||||
mutationFn: (values: TotpSchema) => axios.post("/api/user/totp", values),
|
||||
@@ -39,13 +42,13 @@ export const TotpPage = () => {
|
||||
});
|
||||
|
||||
redirectTimer.current = window.setTimeout(() => {
|
||||
if (oidcParams.isOidc) {
|
||||
window.location.replace(`/authorize?${oidcParams.compiled}`);
|
||||
if (isOidc) {
|
||||
window.location.replace(`/authorize?${compiledOIDCParams}`);
|
||||
return;
|
||||
}
|
||||
|
||||
window.location.replace(
|
||||
`/continue${redirectUri ? `?redirect_uri=${encodeURIComponent(redirectUri)}` : ""}`,
|
||||
`/continue${props.redirect_uri ? `?redirect_uri=${encodeURIComponent(props.redirect_uri)}` : ""}`,
|
||||
);
|
||||
}, 500);
|
||||
},
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
ALTER TABLE "oidc_codes" DROP COLUMN "code_challenge";
|
||||
@@ -1 +0,0 @@
|
||||
ALTER TABLE "oidc_codes" ADD COLUMN "code_challenge" TEXT DEFAULT "";
|
||||
@@ -10,12 +10,10 @@ import (
|
||||
"github.com/steveiliop56/tinyauth/internal/config"
|
||||
"github.com/steveiliop56/tinyauth/internal/controller"
|
||||
"github.com/steveiliop56/tinyauth/internal/utils"
|
||||
"github.com/steveiliop56/tinyauth/internal/utils/tlog"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestContextController(t *testing.T) {
|
||||
tlog.NewTestLogger().Init()
|
||||
controllerConfig := controller.ContextControllerConfig{
|
||||
Providers: []controller.Provider{
|
||||
{
|
||||
|
||||
@@ -8,12 +8,10 @@ import (
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/steveiliop56/tinyauth/internal/controller"
|
||||
"github.com/steveiliop56/tinyauth/internal/utils/tlog"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestHealthController(t *testing.T) {
|
||||
tlog.NewTestLogger().Init()
|
||||
tests := []struct {
|
||||
description string
|
||||
path string
|
||||
|
||||
@@ -9,7 +9,6 @@ import (
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/google/go-querystring/query"
|
||||
|
||||
"github.com/steveiliop56/tinyauth/internal/service"
|
||||
"github.com/steveiliop56/tinyauth/internal/utils"
|
||||
"github.com/steveiliop56/tinyauth/internal/utils/tlog"
|
||||
@@ -35,7 +34,6 @@ type TokenRequest struct {
|
||||
RefreshToken string `form:"refresh_token" url:"refresh_token"`
|
||||
ClientSecret string `form:"client_secret" url:"client_secret"`
|
||||
ClientID string `form:"client_id" url:"client_id"`
|
||||
CodeVerifier string `form:"code_verifier" url:"code_verifier"`
|
||||
}
|
||||
|
||||
type CallbackError struct {
|
||||
@@ -71,7 +69,6 @@ func (controller *OIDCController) SetupRoutes() {
|
||||
oidcGroup.POST("/authorize", controller.Authorize)
|
||||
oidcGroup.POST("/token", controller.Token)
|
||||
oidcGroup.GET("/userinfo", controller.Userinfo)
|
||||
oidcGroup.POST("/userinfo", controller.Userinfo)
|
||||
}
|
||||
|
||||
func (controller *OIDCController) GetClientInfo(c *gin.Context) {
|
||||
@@ -311,16 +308,6 @@ func (controller *OIDCController) Token(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
ok := controller.oidc.ValidatePKCE(entry.CodeChallenge, req.CodeVerifier)
|
||||
|
||||
if !ok {
|
||||
tlog.App.Warn().Msg("PKCE validation failed")
|
||||
c.JSON(400, gin.H{
|
||||
"error": "invalid_grant",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
tokenRes, err := controller.oidc.GenerateAccessToken(c, client, entry)
|
||||
|
||||
if err != nil {
|
||||
@@ -377,48 +364,22 @@ func (controller *OIDCController) Userinfo(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
var token string
|
||||
|
||||
authorization := c.GetHeader("Authorization")
|
||||
if authorization != "" {
|
||||
tokenType, bearerToken, ok := strings.Cut(authorization, " ")
|
||||
if !ok {
|
||||
tlog.App.Warn().Msg("OIDC userinfo accessed with malformed authorization header")
|
||||
c.JSON(401, gin.H{
|
||||
"error": "invalid_request",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if strings.ToLower(tokenType) != "bearer" {
|
||||
tlog.App.Warn().Msg("OIDC userinfo accessed with invalid token type")
|
||||
c.JSON(401, gin.H{
|
||||
"error": "invalid_request",
|
||||
})
|
||||
return
|
||||
}
|
||||
tokenType, token, ok := strings.Cut(authorization, " ")
|
||||
|
||||
token = bearerToken
|
||||
} else if c.Request.Method == http.MethodPost {
|
||||
if c.ContentType() != "application/x-www-form-urlencoded" {
|
||||
tlog.App.Warn().Msg("OIDC userinfo POST accessed with invalid content type")
|
||||
c.JSON(400, gin.H{
|
||||
"error": "invalid_request",
|
||||
})
|
||||
return
|
||||
}
|
||||
token = c.PostForm("access_token")
|
||||
if token == "" {
|
||||
tlog.App.Warn().Msg("OIDC userinfo POST accessed without access_token in body")
|
||||
c.JSON(401, gin.H{
|
||||
"error": "invalid_request",
|
||||
})
|
||||
return
|
||||
}
|
||||
} else {
|
||||
if !ok {
|
||||
tlog.App.Warn().Msg("OIDC userinfo accessed without authorization header")
|
||||
c.JSON(401, gin.H{
|
||||
"error": "invalid_request",
|
||||
"error": "invalid_grant",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if strings.ToLower(tokenType) != "bearer" {
|
||||
tlog.App.Warn().Msg("OIDC userinfo accessed with invalid token type")
|
||||
c.JSON(401, gin.H{
|
||||
"error": "invalid_grant",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
package controller_test
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
@@ -17,13 +15,11 @@ import (
|
||||
"github.com/steveiliop56/tinyauth/internal/controller"
|
||||
"github.com/steveiliop56/tinyauth/internal/repository"
|
||||
"github.com/steveiliop56/tinyauth/internal/service"
|
||||
"github.com/steveiliop56/tinyauth/internal/utils/tlog"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestOIDCController(t *testing.T) {
|
||||
tlog.NewTestLogger().Init()
|
||||
tempDir := t.TempDir()
|
||||
|
||||
oidcServiceCfg := service.OIDCServiceConfig{
|
||||
@@ -435,349 +431,6 @@ func TestOIDCController(t *testing.T) {
|
||||
assert.False(t, ok, "Did not expect email claim in userinfo response")
|
||||
},
|
||||
},
|
||||
{
|
||||
description: "Ensure userinfo forbids access with no authorization header",
|
||||
middlewares: []gin.HandlerFunc{},
|
||||
run: func(t *testing.T, router *gin.Engine, recorder *httptest.ResponseRecorder) {
|
||||
req := httptest.NewRequest("GET", "/api/oidc/userinfo", nil)
|
||||
router.ServeHTTP(recorder, req)
|
||||
assert.Equal(t, 401, recorder.Code)
|
||||
|
||||
var res map[string]any
|
||||
err := json.Unmarshal(recorder.Body.Bytes(), &res)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "invalid_request", res["error"])
|
||||
},
|
||||
},
|
||||
{
|
||||
description: "Ensure userinfo forbids access with malformed authorization header",
|
||||
middlewares: []gin.HandlerFunc{},
|
||||
run: func(t *testing.T, router *gin.Engine, recorder *httptest.ResponseRecorder) {
|
||||
req := httptest.NewRequest("GET", "/api/oidc/userinfo", nil)
|
||||
req.Header.Set("Authorization", "Bearer")
|
||||
router.ServeHTTP(recorder, req)
|
||||
assert.Equal(t, 401, recorder.Code)
|
||||
|
||||
var res map[string]any
|
||||
err := json.Unmarshal(recorder.Body.Bytes(), &res)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "invalid_request", res["error"])
|
||||
},
|
||||
},
|
||||
{
|
||||
description: "Ensure userinfo forbids access with invalid token type",
|
||||
middlewares: []gin.HandlerFunc{},
|
||||
run: func(t *testing.T, router *gin.Engine, recorder *httptest.ResponseRecorder) {
|
||||
req := httptest.NewRequest("GET", "/api/oidc/userinfo", nil)
|
||||
req.Header.Set("Authorization", "Basic some-token")
|
||||
router.ServeHTTP(recorder, req)
|
||||
assert.Equal(t, 401, recorder.Code)
|
||||
|
||||
var res map[string]any
|
||||
err := json.Unmarshal(recorder.Body.Bytes(), &res)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "invalid_request", res["error"])
|
||||
},
|
||||
},
|
||||
{
|
||||
description: "Ensure userinfo forbids access with empty bearer token",
|
||||
middlewares: []gin.HandlerFunc{},
|
||||
run: func(t *testing.T, router *gin.Engine, recorder *httptest.ResponseRecorder) {
|
||||
req := httptest.NewRequest("GET", "/api/oidc/userinfo", nil)
|
||||
req.Header.Set("Authorization", "Bearer ")
|
||||
router.ServeHTTP(recorder, req)
|
||||
assert.Equal(t, 401, recorder.Code)
|
||||
|
||||
var res map[string]any
|
||||
err := json.Unmarshal(recorder.Body.Bytes(), &res)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "invalid_grant", res["error"])
|
||||
},
|
||||
},
|
||||
{
|
||||
description: "Ensure userinfo POST rejects missing access token in body",
|
||||
middlewares: []gin.HandlerFunc{},
|
||||
run: func(t *testing.T, router *gin.Engine, recorder *httptest.ResponseRecorder) {
|
||||
req := httptest.NewRequest("POST", "/api/oidc/userinfo", strings.NewReader(""))
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
router.ServeHTTP(recorder, req)
|
||||
assert.Equal(t, 401, recorder.Code)
|
||||
|
||||
var res map[string]any
|
||||
err := json.Unmarshal(recorder.Body.Bytes(), &res)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "invalid_request", res["error"])
|
||||
},
|
||||
},
|
||||
{
|
||||
description: "Ensure userinfo POST rejects wrong content type",
|
||||
middlewares: []gin.HandlerFunc{},
|
||||
run: func(t *testing.T, router *gin.Engine, recorder *httptest.ResponseRecorder) {
|
||||
req := httptest.NewRequest("POST", "/api/oidc/userinfo", strings.NewReader(`{"access_token":"some-token"}`))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
router.ServeHTTP(recorder, req)
|
||||
assert.Equal(t, 400, recorder.Code)
|
||||
|
||||
var res map[string]any
|
||||
err := json.Unmarshal(recorder.Body.Bytes(), &res)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "invalid_request", res["error"])
|
||||
},
|
||||
},
|
||||
{
|
||||
description: "Ensure userinfo accepts access token via POST body",
|
||||
middlewares: []gin.HandlerFunc{
|
||||
simpleCtx,
|
||||
},
|
||||
run: func(t *testing.T, router *gin.Engine, recorder *httptest.ResponseRecorder) {
|
||||
tokenTest, found := getTestByDescription("Ensure we can get a token with a valid request")
|
||||
assert.True(t, found, "Token test not found")
|
||||
tokenRecorder := httptest.NewRecorder()
|
||||
tokenTest(t, router, tokenRecorder)
|
||||
|
||||
var tokenRes map[string]any
|
||||
err := json.Unmarshal(tokenRecorder.Body.Bytes(), &tokenRes)
|
||||
assert.NoError(t, err)
|
||||
|
||||
accessToken := tokenRes["access_token"].(string)
|
||||
assert.NotEmpty(t, accessToken)
|
||||
|
||||
body := url.Values{}
|
||||
body.Set("access_token", accessToken)
|
||||
req := httptest.NewRequest("POST", "/api/oidc/userinfo", strings.NewReader(body.Encode()))
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
router.ServeHTTP(recorder, req)
|
||||
assert.Equal(t, 200, recorder.Code)
|
||||
|
||||
var userInfoRes map[string]any
|
||||
err = json.Unmarshal(recorder.Body.Bytes(), &userInfoRes)
|
||||
assert.NoError(t, err)
|
||||
|
||||
_, ok := userInfoRes["sub"]
|
||||
assert.True(t, ok, "Expected sub claim in userinfo response")
|
||||
},
|
||||
},
|
||||
{
|
||||
description: "Ensure plain PKCE succeeds",
|
||||
middlewares: []gin.HandlerFunc{
|
||||
simpleCtx,
|
||||
},
|
||||
run: func(t *testing.T, router *gin.Engine, recorder *httptest.ResponseRecorder) {
|
||||
reqBody := service.AuthorizeRequest{
|
||||
Scope: "openid",
|
||||
ResponseType: "code",
|
||||
ClientID: "some-client-id",
|
||||
RedirectURI: "https://test.example.com/callback",
|
||||
State: "some-state",
|
||||
Nonce: "some-nonce",
|
||||
CodeChallenge: "some-challenge",
|
||||
// Not setting a code challenge method should default to "plain"
|
||||
CodeChallengeMethod: "",
|
||||
}
|
||||
reqBodyBytes, err := json.Marshal(reqBody)
|
||||
assert.NoError(t, err)
|
||||
|
||||
req := httptest.NewRequest("POST", "/api/oidc/authorize", strings.NewReader(string(reqBodyBytes)))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
router.ServeHTTP(recorder, req)
|
||||
assert.Equal(t, 200, recorder.Code)
|
||||
|
||||
var res map[string]any
|
||||
err = json.Unmarshal(recorder.Body.Bytes(), &res)
|
||||
assert.NoError(t, err)
|
||||
|
||||
redirectURI := res["redirect_uri"].(string)
|
||||
url, err := url.Parse(redirectURI)
|
||||
assert.NoError(t, err)
|
||||
|
||||
queryParams := url.Query()
|
||||
assert.Equal(t, queryParams.Get("state"), "some-state")
|
||||
|
||||
code := queryParams.Get("code")
|
||||
assert.NotEmpty(t, code)
|
||||
|
||||
// Now exchange the code for a token
|
||||
recorder = httptest.NewRecorder()
|
||||
tokenReqBody := controller.TokenRequest{
|
||||
GrantType: "authorization_code",
|
||||
Code: code,
|
||||
RedirectURI: "https://test.example.com/callback",
|
||||
CodeVerifier: "some-challenge",
|
||||
}
|
||||
reqBodyEncoded, err := query.Values(tokenReqBody)
|
||||
assert.NoError(t, err)
|
||||
|
||||
req = httptest.NewRequest("POST", "/api/oidc/token", strings.NewReader(reqBodyEncoded.Encode()))
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
req.SetBasicAuth("some-client-id", "some-client-secret")
|
||||
router.ServeHTTP(recorder, req)
|
||||
|
||||
assert.Equal(t, 200, recorder.Code)
|
||||
},
|
||||
},
|
||||
{
|
||||
description: "Ensure S256 PKCE succeeds",
|
||||
middlewares: []gin.HandlerFunc{
|
||||
simpleCtx,
|
||||
},
|
||||
run: func(t *testing.T, router *gin.Engine, recorder *httptest.ResponseRecorder) {
|
||||
hasher := sha256.New()
|
||||
hasher.Write([]byte("some-challenge"))
|
||||
codeChallenge := hasher.Sum(nil)
|
||||
codeChallengeEncoded := base64.RawURLEncoding.EncodeToString(codeChallenge)
|
||||
reqBody := service.AuthorizeRequest{
|
||||
Scope: "openid",
|
||||
ResponseType: "code",
|
||||
ClientID: "some-client-id",
|
||||
RedirectURI: "https://test.example.com/callback",
|
||||
State: "some-state",
|
||||
Nonce: "some-nonce",
|
||||
CodeChallenge: codeChallengeEncoded,
|
||||
CodeChallengeMethod: "S256",
|
||||
}
|
||||
reqBodyBytes, err := json.Marshal(reqBody)
|
||||
assert.NoError(t, err)
|
||||
|
||||
req := httptest.NewRequest("POST", "/api/oidc/authorize", strings.NewReader(string(reqBodyBytes)))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
router.ServeHTTP(recorder, req)
|
||||
assert.Equal(t, 200, recorder.Code)
|
||||
|
||||
var res map[string]any
|
||||
err = json.Unmarshal(recorder.Body.Bytes(), &res)
|
||||
assert.NoError(t, err)
|
||||
|
||||
redirectURI := res["redirect_uri"].(string)
|
||||
url, err := url.Parse(redirectURI)
|
||||
assert.NoError(t, err)
|
||||
|
||||
queryParams := url.Query()
|
||||
assert.Equal(t, queryParams.Get("state"), "some-state")
|
||||
|
||||
code := queryParams.Get("code")
|
||||
assert.NotEmpty(t, code)
|
||||
|
||||
// Now exchange the code for a token
|
||||
recorder = httptest.NewRecorder()
|
||||
tokenReqBody := controller.TokenRequest{
|
||||
GrantType: "authorization_code",
|
||||
Code: code,
|
||||
RedirectURI: "https://test.example.com/callback",
|
||||
CodeVerifier: "some-challenge",
|
||||
}
|
||||
reqBodyEncoded, err := query.Values(tokenReqBody)
|
||||
assert.NoError(t, err)
|
||||
|
||||
req = httptest.NewRequest("POST", "/api/oidc/token", strings.NewReader(reqBodyEncoded.Encode()))
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
req.SetBasicAuth("some-client-id", "some-client-secret")
|
||||
router.ServeHTTP(recorder, req)
|
||||
|
||||
assert.Equal(t, 200, recorder.Code)
|
||||
},
|
||||
},
|
||||
{
|
||||
description: "Ensure request with invalid PKCE fails",
|
||||
middlewares: []gin.HandlerFunc{
|
||||
simpleCtx,
|
||||
},
|
||||
run: func(t *testing.T, router *gin.Engine, recorder *httptest.ResponseRecorder) {
|
||||
hasher := sha256.New()
|
||||
hasher.Write([]byte("some-challenge"))
|
||||
codeChallenge := hasher.Sum(nil)
|
||||
codeChallengeEncoded := base64.RawURLEncoding.EncodeToString(codeChallenge)
|
||||
reqBody := service.AuthorizeRequest{
|
||||
Scope: "openid",
|
||||
ResponseType: "code",
|
||||
ClientID: "some-client-id",
|
||||
RedirectURI: "https://test.example.com/callback",
|
||||
State: "some-state",
|
||||
Nonce: "some-nonce",
|
||||
CodeChallenge: codeChallengeEncoded,
|
||||
CodeChallengeMethod: "S256",
|
||||
}
|
||||
reqBodyBytes, err := json.Marshal(reqBody)
|
||||
assert.NoError(t, err)
|
||||
|
||||
req := httptest.NewRequest("POST", "/api/oidc/authorize", strings.NewReader(string(reqBodyBytes)))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
router.ServeHTTP(recorder, req)
|
||||
assert.Equal(t, 200, recorder.Code)
|
||||
|
||||
var res map[string]any
|
||||
err = json.Unmarshal(recorder.Body.Bytes(), &res)
|
||||
assert.NoError(t, err)
|
||||
|
||||
redirectURI := res["redirect_uri"].(string)
|
||||
url, err := url.Parse(redirectURI)
|
||||
assert.NoError(t, err)
|
||||
|
||||
queryParams := url.Query()
|
||||
assert.Equal(t, queryParams.Get("state"), "some-state")
|
||||
|
||||
code := queryParams.Get("code")
|
||||
assert.NotEmpty(t, code)
|
||||
|
||||
// Now exchange the code for a token
|
||||
recorder = httptest.NewRecorder()
|
||||
tokenReqBody := controller.TokenRequest{
|
||||
GrantType: "authorization_code",
|
||||
Code: code,
|
||||
RedirectURI: "https://test.example.com/callback",
|
||||
CodeVerifier: "some-challenge-1",
|
||||
}
|
||||
reqBodyEncoded, err := query.Values(tokenReqBody)
|
||||
assert.NoError(t, err)
|
||||
|
||||
req = httptest.NewRequest("POST", "/api/oidc/token", strings.NewReader(reqBodyEncoded.Encode()))
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
req.SetBasicAuth("some-client-id", "some-client-secret")
|
||||
router.ServeHTTP(recorder, req)
|
||||
|
||||
assert.Equal(t, 400, recorder.Code)
|
||||
},
|
||||
},
|
||||
{
|
||||
description: "Ensure request with invalid challenge method fails",
|
||||
middlewares: []gin.HandlerFunc{
|
||||
simpleCtx,
|
||||
},
|
||||
run: func(t *testing.T, router *gin.Engine, recorder *httptest.ResponseRecorder) {
|
||||
hasher := sha256.New()
|
||||
hasher.Write([]byte("some-challenge"))
|
||||
codeChallenge := hasher.Sum(nil)
|
||||
codeChallengeEncoded := base64.RawURLEncoding.EncodeToString(codeChallenge)
|
||||
reqBody := service.AuthorizeRequest{
|
||||
Scope: "openid",
|
||||
ResponseType: "code",
|
||||
ClientID: "some-client-id",
|
||||
RedirectURI: "https://test.example.com/callback",
|
||||
State: "some-state",
|
||||
Nonce: "some-nonce",
|
||||
CodeChallenge: codeChallengeEncoded,
|
||||
CodeChallengeMethod: "foo",
|
||||
}
|
||||
reqBodyBytes, err := json.Marshal(reqBody)
|
||||
assert.NoError(t, err)
|
||||
|
||||
req := httptest.NewRequest("POST", "/api/oidc/authorize", strings.NewReader(string(reqBodyBytes)))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
router.ServeHTTP(recorder, req)
|
||||
assert.Equal(t, 200, recorder.Code)
|
||||
|
||||
var res map[string]any
|
||||
err = json.Unmarshal(recorder.Body.Bytes(), &res)
|
||||
assert.NoError(t, err)
|
||||
|
||||
redirectURI := res["redirect_uri"].(string)
|
||||
url, err := url.Parse(redirectURI)
|
||||
assert.NoError(t, err)
|
||||
|
||||
queryParams := url.Query()
|
||||
error := queryParams.Get("error")
|
||||
assert.NotEmpty(t, error)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
app := bootstrap.NewBootstrapApp(config.Config{})
|
||||
|
||||
@@ -17,7 +17,6 @@ import (
|
||||
)
|
||||
|
||||
func TestProxyController(t *testing.T) {
|
||||
tlog.NewTestLogger().Init()
|
||||
tempDir := t.TempDir()
|
||||
|
||||
authServiceCfg := service.AuthServiceConfig{
|
||||
@@ -391,6 +390,8 @@ func TestProxyController(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
tlog.NewSimpleLogger().Init()
|
||||
|
||||
oauthBrokerCfgs := make(map[string]config.OAuthServiceConfig)
|
||||
|
||||
app := bootstrap.NewBootstrapApp(config.Config{})
|
||||
|
||||
@@ -8,13 +8,11 @@ import (
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/steveiliop56/tinyauth/internal/controller"
|
||||
"github.com/steveiliop56/tinyauth/internal/utils/tlog"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestResourcesController(t *testing.T) {
|
||||
tlog.NewTestLogger().Init()
|
||||
tempDir := t.TempDir()
|
||||
|
||||
resourcesControllerCfg := controller.ResourcesControllerConfig{
|
||||
|
||||
@@ -22,7 +22,6 @@ import (
|
||||
)
|
||||
|
||||
func TestUserController(t *testing.T) {
|
||||
tlog.NewTestLogger().Init()
|
||||
tempDir := t.TempDir()
|
||||
|
||||
authServiceCfg := service.AuthServiceConfig{
|
||||
@@ -275,6 +274,8 @@ func TestUserController(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
tlog.NewSimpleLogger().Init()
|
||||
|
||||
oauthBrokerCfgs := make(map[string]config.OAuthServiceConfig)
|
||||
|
||||
app := bootstrap.NewBootstrapApp(config.Config{})
|
||||
|
||||
@@ -13,13 +13,11 @@ import (
|
||||
"github.com/steveiliop56/tinyauth/internal/controller"
|
||||
"github.com/steveiliop56/tinyauth/internal/repository"
|
||||
"github.com/steveiliop56/tinyauth/internal/service"
|
||||
"github.com/steveiliop56/tinyauth/internal/utils/tlog"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestWellKnownController(t *testing.T) {
|
||||
tlog.NewTestLogger().Init()
|
||||
tempDir := t.TempDir()
|
||||
|
||||
oidcServiceCfg := service.OIDCServiceConfig{
|
||||
|
||||
@@ -24,7 +24,6 @@ var (
|
||||
"GET /api/oidc/clients",
|
||||
"POST /api/oidc/token",
|
||||
"GET /api/oidc/userinfo",
|
||||
"POST /api/oidc/userinfo",
|
||||
"GET /resources",
|
||||
"POST /api/user/login",
|
||||
"GET /.well-known/openid-configuration",
|
||||
|
||||
@@ -5,14 +5,13 @@
|
||||
package repository
|
||||
|
||||
type OidcCode struct {
|
||||
Sub string
|
||||
CodeHash string
|
||||
Scope string
|
||||
RedirectURI string
|
||||
ClientID string
|
||||
ExpiresAt int64
|
||||
Nonce string
|
||||
CodeChallenge string
|
||||
Sub string
|
||||
CodeHash string
|
||||
Scope string
|
||||
RedirectURI string
|
||||
ClientID string
|
||||
ExpiresAt int64
|
||||
Nonce string
|
||||
}
|
||||
|
||||
type OidcToken struct {
|
||||
|
||||
@@ -17,23 +17,21 @@ INSERT INTO "oidc_codes" (
|
||||
"redirect_uri",
|
||||
"client_id",
|
||||
"expires_at",
|
||||
"nonce",
|
||||
"code_challenge"
|
||||
"nonce"
|
||||
) VALUES (
|
||||
?, ?, ?, ?, ?, ?, ?, ?
|
||||
?, ?, ?, ?, ?, ?, ?
|
||||
)
|
||||
RETURNING sub, code_hash, scope, redirect_uri, client_id, expires_at, nonce, code_challenge
|
||||
RETURNING sub, code_hash, scope, redirect_uri, client_id, expires_at, nonce
|
||||
`
|
||||
|
||||
type CreateOidcCodeParams struct {
|
||||
Sub string
|
||||
CodeHash string
|
||||
Scope string
|
||||
RedirectURI string
|
||||
ClientID string
|
||||
ExpiresAt int64
|
||||
Nonce string
|
||||
CodeChallenge string
|
||||
Sub string
|
||||
CodeHash string
|
||||
Scope string
|
||||
RedirectURI string
|
||||
ClientID string
|
||||
ExpiresAt int64
|
||||
Nonce string
|
||||
}
|
||||
|
||||
func (q *Queries) CreateOidcCode(ctx context.Context, arg CreateOidcCodeParams) (OidcCode, error) {
|
||||
@@ -45,7 +43,6 @@ func (q *Queries) CreateOidcCode(ctx context.Context, arg CreateOidcCodeParams)
|
||||
arg.ClientID,
|
||||
arg.ExpiresAt,
|
||||
arg.Nonce,
|
||||
arg.CodeChallenge,
|
||||
)
|
||||
var i OidcCode
|
||||
err := row.Scan(
|
||||
@@ -56,7 +53,6 @@ func (q *Queries) CreateOidcCode(ctx context.Context, arg CreateOidcCodeParams)
|
||||
&i.ClientID,
|
||||
&i.ExpiresAt,
|
||||
&i.Nonce,
|
||||
&i.CodeChallenge,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
@@ -160,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, nonce, code_challenge
|
||||
RETURNING sub, code_hash, scope, redirect_uri, client_id, expires_at, nonce
|
||||
`
|
||||
|
||||
func (q *Queries) DeleteExpiredOidcCodes(ctx context.Context, expiresAt int64) ([]OidcCode, error) {
|
||||
@@ -180,7 +176,6 @@ func (q *Queries) DeleteExpiredOidcCodes(ctx context.Context, expiresAt int64) (
|
||||
&i.ClientID,
|
||||
&i.ExpiresAt,
|
||||
&i.Nonce,
|
||||
&i.CodeChallenge,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -291,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, nonce, code_challenge
|
||||
RETURNING sub, code_hash, scope, redirect_uri, client_id, expires_at, nonce
|
||||
`
|
||||
|
||||
func (q *Queries) GetOidcCode(ctx context.Context, codeHash string) (OidcCode, error) {
|
||||
@@ -305,7 +300,6 @@ func (q *Queries) GetOidcCode(ctx context.Context, codeHash string) (OidcCode, e
|
||||
&i.ClientID,
|
||||
&i.ExpiresAt,
|
||||
&i.Nonce,
|
||||
&i.CodeChallenge,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
@@ -313,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, nonce, code_challenge
|
||||
RETURNING sub, code_hash, scope, redirect_uri, client_id, expires_at, nonce
|
||||
`
|
||||
|
||||
func (q *Queries) GetOidcCodeBySub(ctx context.Context, sub string) (OidcCode, error) {
|
||||
@@ -327,13 +321,12 @@ func (q *Queries) GetOidcCodeBySub(ctx context.Context, sub string) (OidcCode, e
|
||||
&i.ClientID,
|
||||
&i.ExpiresAt,
|
||||
&i.Nonce,
|
||||
&i.CodeChallenge,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const getOidcCodeBySubUnsafe = `-- name: GetOidcCodeBySubUnsafe :one
|
||||
SELECT sub, code_hash, scope, redirect_uri, client_id, expires_at, nonce, code_challenge FROM "oidc_codes"
|
||||
SELECT sub, code_hash, scope, redirect_uri, client_id, expires_at, nonce FROM "oidc_codes"
|
||||
WHERE "sub" = ?
|
||||
`
|
||||
|
||||
@@ -348,13 +341,12 @@ func (q *Queries) GetOidcCodeBySubUnsafe(ctx context.Context, sub string) (OidcC
|
||||
&i.ClientID,
|
||||
&i.ExpiresAt,
|
||||
&i.Nonce,
|
||||
&i.CodeChallenge,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const getOidcCodeUnsafe = `-- name: GetOidcCodeUnsafe :one
|
||||
SELECT sub, code_hash, scope, redirect_uri, client_id, expires_at, nonce, code_challenge FROM "oidc_codes"
|
||||
SELECT sub, code_hash, scope, redirect_uri, client_id, expires_at, nonce FROM "oidc_codes"
|
||||
WHERE "code_hash" = ?
|
||||
`
|
||||
|
||||
@@ -369,7 +361,6 @@ func (q *Queries) GetOidcCodeUnsafe(ctx context.Context, codeHash string) (OidcC
|
||||
&i.ClientID,
|
||||
&i.ExpiresAt,
|
||||
&i.Nonce,
|
||||
&i.CodeChallenge,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
@@ -75,14 +75,12 @@ type TokenResponse struct {
|
||||
}
|
||||
|
||||
type AuthorizeRequest struct {
|
||||
Scope string `json:"scope" binding:"required"`
|
||||
ResponseType string `json:"response_type" binding:"required"`
|
||||
ClientID string `json:"client_id" binding:"required"`
|
||||
RedirectURI string `json:"redirect_uri" binding:"required"`
|
||||
State string `json:"state"`
|
||||
Nonce string `json:"nonce"`
|
||||
CodeChallenge string `json:"code_challenge"`
|
||||
CodeChallengeMethod string `json:"code_challenge_method"`
|
||||
Scope string `json:"scope" binding:"required"`
|
||||
ResponseType string `json:"response_type" binding:"required"`
|
||||
ClientID string `json:"client_id" binding:"required"`
|
||||
RedirectURI string `json:"redirect_uri" binding:"required"`
|
||||
State string `json:"state"`
|
||||
Nonce string `json:"nonce"`
|
||||
}
|
||||
|
||||
type OIDCServiceConfig struct {
|
||||
@@ -295,13 +293,6 @@ func (service *OIDCService) ValidateAuthorizeParams(req AuthorizeRequest) error
|
||||
return errors.New("invalid_request_uri")
|
||||
}
|
||||
|
||||
// PKCE code challenge method if set
|
||||
if req.CodeChallenge != "" && req.CodeChallengeMethod != "" {
|
||||
if req.CodeChallengeMethod != "S256" && req.CodeChallengeMethod != "plain" {
|
||||
return errors.New("invalid_request")
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -315,7 +306,8 @@ func (service *OIDCService) StoreCode(c *gin.Context, sub string, code string, r
|
||||
// Fixed 10 minutes
|
||||
expiresAt := time.Now().Add(time.Minute * time.Duration(10)).Unix()
|
||||
|
||||
entry := repository.CreateOidcCodeParams{
|
||||
// Insert the code into the database
|
||||
_, err := service.queries.CreateOidcCode(c, repository.CreateOidcCodeParams{
|
||||
Sub: sub,
|
||||
CodeHash: service.Hash(code),
|
||||
// Here it's safe to split and trust the output since, we validated the scopes before
|
||||
@@ -324,19 +316,7 @@ func (service *OIDCService) StoreCode(c *gin.Context, sub string, code string, r
|
||||
ClientID: req.ClientID,
|
||||
ExpiresAt: expiresAt,
|
||||
Nonce: req.Nonce,
|
||||
}
|
||||
|
||||
if req.CodeChallenge != "" {
|
||||
if req.CodeChallengeMethod == "S256" {
|
||||
entry.CodeChallenge = req.CodeChallenge
|
||||
} else {
|
||||
entry.CodeChallenge = service.hashAndEncodePKCE(req.CodeChallenge)
|
||||
tlog.App.Warn().Msg("Received plain PKCE code challenge, it's recommended to use S256 for better security")
|
||||
}
|
||||
}
|
||||
|
||||
// Insert the code into the database
|
||||
_, err := service.queries.CreateOidcCode(c, entry)
|
||||
})
|
||||
|
||||
return err
|
||||
}
|
||||
@@ -748,16 +728,3 @@ func (service *OIDCService) GetJWK() ([]byte, error) {
|
||||
|
||||
return jwk.Public().MarshalJSON()
|
||||
}
|
||||
|
||||
func (service *OIDCService) ValidatePKCE(codeChallenge string, codeVerifier string) bool {
|
||||
if codeChallenge == "" {
|
||||
return true
|
||||
}
|
||||
return codeChallenge == service.hashAndEncodePKCE(codeVerifier)
|
||||
}
|
||||
|
||||
func (service *OIDCService) hashAndEncodePKCE(codeVerifier string) string {
|
||||
hasher := sha256.New()
|
||||
hasher.Write([]byte(codeVerifier))
|
||||
return base64.RawURLEncoding.EncodeToString(hasher.Sum(nil))
|
||||
}
|
||||
|
||||
@@ -55,17 +55,6 @@ func NewSimpleLogger() *Logger {
|
||||
})
|
||||
}
|
||||
|
||||
func NewTestLogger() *Logger {
|
||||
return NewLogger(config.LogConfig{
|
||||
Level: "trace",
|
||||
Streams: config.LogStreams{
|
||||
HTTP: config.LogStreamConfig{Enabled: true},
|
||||
App: config.LogStreamConfig{Enabled: true},
|
||||
Audit: config.LogStreamConfig{Enabled: true},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func (l *Logger) Init() {
|
||||
Audit = l.Audit
|
||||
HTTP = l.HTTP
|
||||
|
||||
@@ -6,10 +6,9 @@ INSERT INTO "oidc_codes" (
|
||||
"redirect_uri",
|
||||
"client_id",
|
||||
"expires_at",
|
||||
"nonce",
|
||||
"code_challenge"
|
||||
"nonce"
|
||||
) VALUES (
|
||||
?, ?, ?, ?, ?, ?, ?, ?
|
||||
?, ?, ?, ?, ?, ?, ?
|
||||
)
|
||||
RETURNING *;
|
||||
|
||||
|
||||
@@ -5,8 +5,7 @@ CREATE TABLE IF NOT EXISTS "oidc_codes" (
|
||||
"redirect_uri" TEXT NOT NULL,
|
||||
"client_id" TEXT NOT NULL,
|
||||
"expires_at" INTEGER NOT NULL,
|
||||
"nonce" TEXT DEFAULT "",
|
||||
"code_challenge" TEXT DEFAULT ""
|
||||
"nonce" TEXT DEFAULT ""
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS "oidc_tokens" (
|
||||
|
||||
Reference in New Issue
Block a user