Skip to content

Commit 809e80b

Browse files
committed
wip: checking in
1 parent fd08400 commit 809e80b

File tree

5 files changed

+93
-12
lines changed

5 files changed

+93
-12
lines changed

playground/helmfile/platform/ory/kratos-values.yaml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ kratos:
3333
allowed_return_urls:
3434
- https://forge.projectcatalyst.dev/
3535
- https://auth.projectcatalyst.dev/api/v1/oauth2/login
36+
- https://auth.projectcatalyst.dev/api/v1/oauth2/consent
3637
flows:
3738
settings:
3839
ui_url: https://forge.projectcatalyst.dev//profile
@@ -70,6 +71,11 @@ kratos:
7071
- email
7172
- profile
7273

74+
cookies:
75+
domain: projectcatalyst.dev
76+
path: /
77+
same_site: Lax
78+
7379
session:
7480
cookie:
7581
domain: projectcatalyst.dev

playground/test_cli_auth.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,8 @@ def run_server(server_class=HTTPServer, handler_class=CallbackHandler, port=4915
5858
# 2. Open Browser and Start Server
5959
print("Opening browser for authentication...")
6060
time.sleep(2)
61-
webbrowser.open(auth_url)
61+
print(auth_url)
62+
#webbrowser.open(auth_url)
6263
authorization_code = None
6364
received_state = None
6465
run_server()

services/frontend/src/lib/auth/oidc.ts

Lines changed: 31 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -33,31 +33,51 @@ const kratos = new FrontendApi(
3333
* browser naturally follows redirects (no CORS surprises).
3434
*/
3535
export async function loginWithProvider(provider: string, returnTo: string = "/"): Promise<void> {
36-
async function getFlowWithAction(): Promise<HasUi | null> {
36+
// Prefer reusing an existing Kratos login flow if the URL already contains ?flow=...
37+
// This preserves the original return_to (e.g., the Hydra login_challenge callback)
38+
console.log("STARTING LOGIN WITH PROVIDER", provider, returnTo);
39+
const url = new URL(window.location.href);
40+
const existingFlowId = url.searchParams.get("flow");
41+
console.log("existingFlowId", existingFlowId);
42+
43+
// For custom UI: read the existing flow using a no-credentials JSON request.
44+
// Do NOT send cookies on this request or Kratos will require a CSRF header and return 403.
45+
async function readExistingFlow(flowId: string): Promise<HasUi | null> {
3746
try {
38-
const { data } = await kratos.createBrowserLoginFlow();
39-
const f = data as unknown as HasUi;
47+
const { data } = await kratos.getLoginFlow({ id: flowId });
48+
const f = data as HasUi;
4049
const hasAction = typeof f.ui?.action === "string" && f.ui.action.length > 0;
4150
const hasId = typeof f.id === "string" && f.id.length > 0;
42-
if (hasAction || hasId) return f;
43-
return null;
44-
} catch {
51+
return (hasAction || hasId) ? f : null;
52+
} catch (err) {
53+
console.log("getLoginFlow error", err);
4554
return null;
4655
}
4756
}
4857

49-
let flow = await getFlowWithAction();
50-
if (!flow) flow = await getFlowWithAction();
58+
let flow: HasUi | null = null;
59+
60+
// Try to load the existing flow first (if present in URL)
61+
if (existingFlowId) {
62+
try {
63+
console.log("readExistingFlow", existingFlowId);
64+
flow = await readExistingFlow(existingFlowId);
65+
} catch {
66+
console.log("readExistingFlow error", existingFlowId);
67+
}
68+
}
5169

70+
// If no existing flow, initialize one via browser endpoint so Kratos sets cookies & returns here with ?flow=
5271
if (!flow) {
53-
// As a last resort, navigate to the browser endpoint to let Kratos drive the flow
54-
window.location.href = `${kratosBase}/self-service/login/browser`;
72+
const rt = returnTo && returnTo.length > 0 ? `?return_to=${encodeURIComponent(returnTo)}` : "";
73+
window.location.href = `${kratosBase}/self-service/login/browser${rt}`;
5574
return;
5675
}
5776

5877
const action = flow.ui?.action ?? (flow.id ? `${kratosBase}/self-service/login?flow=${flow.id}` : "");
5978
if (!action) {
60-
window.location.href = `${kratosBase}/self-service/login/browser`;
79+
const rt = returnTo && returnTo.length > 0 ? `?return_to=${encodeURIComponent(returnTo)}` : "";
80+
window.location.href = `${kratosBase}/self-service/login/browser${rt}`;
6181
return;
6282
}
6383
const method = (flow.ui?.method ?? "POST").toUpperCase();

services/ory/auth/internal/api/router.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,9 @@ func SetupRouter(cfg *config.Config) *gin.Engine {
5050
v1.POST("/oauth2/consent", h.ConsentPost)
5151
v1.POST("/hydra/token-hook", h.TokenHook)
5252

53+
// BFF: expose Kratos login flow JSON without cookies for SPA
54+
v1.GET("/kratos/login-flow", h.KratosLoginFlow)
55+
5356
// Prometheus metrics endpoint
5457
v1.GET("/metrics", gin.WrapH(promhttp.Handler()))
5558
}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
package handlers
2+
3+
import (
4+
"fmt"
5+
"io"
6+
"net/http"
7+
"net/url"
8+
9+
"github.com/gin-gonic/gin"
10+
)
11+
12+
// KratosLoginFlow proxies the Kratos login flow JSON without forwarding cookies.
13+
// It is intended for SPAs to read the flow (including csrf_token and ui.action)
14+
// without tripping CSRF protections.
15+
func (h *Handlers) KratosLoginFlow(c *gin.Context) {
16+
flowID := c.Query("id")
17+
if flowID == "" {
18+
c.JSON(http.StatusBadRequest, gin.H{"error": "missing id", "correlation": corrFields(c)})
19+
return
20+
}
21+
22+
endpoint := fmt.Sprintf("%s/self-service/login/flows?id=%s", h.cfg.Kratos.PublicURL, url.QueryEscape(flowID))
23+
req, err := http.NewRequest(http.MethodGet, endpoint, nil)
24+
if err != nil {
25+
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error(), "correlation": corrFields(c)})
26+
return
27+
}
28+
// Ensure no cookies are forwarded; set minimal headers
29+
req.Header.Set("Accept", "application/json")
30+
31+
resp, err := h.httpClient.Do(req)
32+
if err != nil {
33+
c.JSON(http.StatusBadGateway, gin.H{"error": err.Error(), "correlation": corrFields(c)})
34+
return
35+
}
36+
defer resp.Body.Close()
37+
38+
body, err := io.ReadAll(resp.Body)
39+
if err != nil {
40+
c.JSON(http.StatusBadGateway, gin.H{"error": err.Error(), "correlation": corrFields(c)})
41+
return
42+
}
43+
44+
contentType := resp.Header.Get("Content-Type")
45+
if contentType == "" {
46+
contentType = "application/json"
47+
}
48+
// Disable caching of flow JSON
49+
c.Header("Cache-Control", "no-store, no-cache, must-revalidate, private")
50+
c.Data(resp.StatusCode, contentType, body)
51+
}

0 commit comments

Comments
 (0)