Skip to content

Commit 1150f09

Browse files
committed
✨(frontend) working oidc connection
Move from usequery to simple use effect, with hard coded response mode to query for our usage
1 parent 1f56edf commit 1150f09

5 files changed

Lines changed: 175 additions & 260 deletions

File tree

src/frontend/apps/hub/src/features/auth/Auth.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ export const Auth = ({ children }: PropsWithChildren) => {
4040
const { config } = useConfig();
4141

4242
// Fetch Matrix chat user credentials when regular user is authenticated
43-
const { chatUser } = useMatrixChatUser(user);
43+
const { chatUser, isProcessingCallback, isStartOidcFlow } = useMatrixChatUser(user);
4444

4545
const init = async () => {
4646
try {
@@ -84,7 +84,7 @@ export const Auth = ({ children }: PropsWithChildren) => {
8484
}
8585
}, [user]);
8686

87-
if (user === undefined) {
87+
if (user === undefined || isProcessingCallback || isStartOidcFlow) {
8888
return (
8989
<div className="hub-auth-loader">
9090
<Spinner size="xl" />
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import { fetchAPI } from "@/features/api/fetchApi";
2+
3+
import { Driver, UserFilters } from "../Driver";
4+
import { ApiConfig, User } from "../types";
5+
6+
/**
7+
* Driver wired to the real Django backend. Only methods that already have a
8+
* matching endpoint are implemented here — chat-related methods are abstract
9+
* and provided by `MockDriver` until the backend ships. Once it does, fold
10+
* those implementations back into this class and delete `MockDriver`.
11+
*/
12+
export abstract class MatrixDriver extends Driver {
13+
async getConfig(): Promise<ApiConfig> {
14+
const response = await fetchAPI(`config/`);
15+
const data = await response.json();
16+
return data;
17+
}
18+
19+
async getUsers(filters?: UserFilters): Promise<User[]> {
20+
const response = await fetchAPI(`users/`, {
21+
params: filters,
22+
});
23+
const data = await response.json();
24+
return data;
25+
}
26+
27+
async updateUser(payload: Partial<User> & { id: string }): Promise<User> {
28+
const response = await fetchAPI(`users/${payload.id}/`, {
29+
method: "PATCH",
30+
body: JSON.stringify(payload),
31+
});
32+
const data = await response.json();
33+
return data;
34+
}
35+
}
Lines changed: 64 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import { User } from "@/features/auth/types";
2-
import { useQuery } from "@tanstack/react-query";
32
import { useRouter } from "next/router";
4-
import { useState } from "react";
3+
import { useEffect, useState } from "react";
54
import { matrixUserStore } from "../stores/matrixUserStore";
5+
import { MatrixUserInterface } from "../types";
66
import {
77
completeOidcLogin,
88
getOIDCAuthUrl,
@@ -11,77 +11,94 @@ import {
1111
import { fetchHomeserverForEmail } from "../utils/autodiscovery";
1212

1313
const OIDC_HS = 'oidc_hs';
14-
const OIDC_RESPONSE_MODE = 'oidc_response_mode';
1514

1615
export const useMatrixChatUser = (user: User | null | undefined) => {
1716
const router = useRouter();
17+
const [chatUser, setChatUser] = useState<MatrixUserInterface | null>(null);
1818
const [isProcessingCallback, setIsProcessingCallback] = useState(false);
19+
const [isStartOidcFlow, setisStartOidcFlow] = useState(false);
1920

20-
const { data: chatUser = null } = useQuery({
21-
queryKey: ['useMatrixChatUser', user], // Refetch when chatUser changes
22-
queryFn: async () => {
21+
useEffect(() => {
22+
if (!user) return;
23+
24+
const initializeUser = async () => {
2325
const currentUser = matrixUserStore.getUser();
26+
let currentHomeserverSelected = sessionStorage.getItem(OIDC_HS);
27+
2428
if (currentUser) {
2529
console.log('**** already has user');
2630
return currentUser;
2731
}
28-
let currentHomeserverSelected = sessionStorage.get(OIDC_HS);
29-
// Check if we're in OIDC callback
30-
const { code, state } = router.query;
31-
if (code && state) {
32-
console.log('**** Processing OIDC callback');
33-
setIsProcessingCallback(true);
34-
const responseMode = sessionStorage.get(OIDC_RESPONSE_MODE) || 'fragment';
35-
const oidcResult = await completeOidcLogin({ code, state }, responseMode);
36-
const {
37-
user_id: userId,
38-
device_id: deviceId,
39-
is_guest: isGuest,
40-
} = await getUserIdFromAccessToken(oidcResult.accessToken, currentHomeserverSelected);
41-
42-
const matrixUser = {
32+
33+
console.log('**** No user found, fetching new user');
34+
setisStartOidcFlow(true);
35+
// Find correct homeserver url
36+
// TODO Should use user?.email, for now only using hardcoded email
37+
let email = user?.email || '';
38+
if (process.env.NODE_ENV === 'development') {
39+
email = 'marc3@tchap.beta.gouv.fr';
40+
}
41+
if (!currentHomeserverSelected) {
42+
const hs = await fetchHomeserverForEmail(email);
43+
currentHomeserverSelected = hs!.base_url;
44+
sessionStorage.setItem(OIDC_HS, hs!.base_url);
45+
}
46+
const authUrl = await getOIDCAuthUrl(currentHomeserverSelected, email);
47+
setisStartOidcFlow(false);
48+
// start oidc flow in tchap MAS
49+
window.location.href = authUrl;
50+
}
51+
52+
initializeUser();
53+
54+
}, [user]);
55+
56+
// Effect 1: Handle OIDC callback (independent of user prop)
57+
useEffect(() => {
58+
if (!router.isReady) return;
59+
const { code, state } = router.query;
60+
console.log("*** code, state", code, state);
61+
if (!code || !state || typeof code !== 'string' || typeof state !== 'string') return;
62+
console.log("*** processing callback oidc");
63+
64+
const processCallback = async () => {
65+
setIsProcessingCallback(true);
66+
try {
67+
const currentHomeserverSelected = sessionStorage.getItem(OIDC_HS);
68+
const oidcResult = await completeOidcLogin({ code, state });
69+
70+
if (!currentHomeserverSelected) {
71+
throw new Error("No homeserver was set");
72+
}
73+
74+
const { user_id: userId, device_id: deviceId, is_guest: isGuest } =
75+
await getUserIdFromAccessToken(oidcResult.accessToken, currentHomeserverSelected);
76+
77+
const matrixUser: MatrixUserInterface = {
4378
homeserverUrl: currentHomeserverSelected,
4479
mxId: userId,
4580
deviceId: deviceId,
4681
accessToken: oidcResult.accessToken,
4782
refreshToken: oidcResult.refreshToken,
4883
guest: isGuest,
4984
};
85+
5086
matrixUserStore.saveUser(matrixUser);
5187
matrixUserStore.persistOIDC(
5288
oidcResult.clientId,
5389
oidcResult.issuer,
5490
oidcResult.idToken,
5591
);
56-
// Clean up URL params
92+
93+
setChatUser(matrixUser);
5794
router.replace(router.pathname, undefined, { shallow: true });
95+
} finally {
5896
setIsProcessingCallback(false);
59-
return matrixUser;
6097
}
98+
};
6199

62-
console.log('**** No user found, fetching new user');
63-
// Find correct homeserver url
64-
// TODO Should use user?.email, for now only using hardcoded email
65-
let email = user?.email || '';
66-
if (process.env.NODE_ENV === 'development') {
67-
email = 'marc3@tchap.beta.gouv.fr';
68-
}
69-
if (!currentHomeserverSelected) {
70-
const hs = await fetchHomeserverForEmail(email);
71-
currentHomeserverSelected = hs!.base_url;
72-
sessionStorage.setItem(OIDC_HS, hs!.base_url);
73-
}
74-
const { authUrl, responseMode } = await getOIDCAuthUrl(currentHomeserverSelected, email);
75-
sessionStorage.setItem(OIDC_RESPONSE_MODE, responseMode);
100+
processCallback();
101+
}, [router.isReady, router.query.code, router.query.state, router.pathname]);
76102

77-
// start oidc flow in tchap MAS
78-
window.location.href = authUrl;
79-
return null;
80-
},
81-
enabled: !!user, // Only run query when user exists
82-
staleTime: Infinity, // Client doesn't stale
83-
retry: 1,
84-
});
85-
86-
return { chatUser, isProcessingCallback };
103+
return { chatUser, isProcessingCallback, isStartOidcFlow };
87104
};

src/frontend/apps/hub/src/features/matrix/utils/auth.ts

Lines changed: 12 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,10 @@ import { secureRandomString } from "matrix-js-sdk/lib/randomstring";
1111
import { matrixConfig } from "../config";
1212
import { CompleteOidcLoginResponse, MatrixUserInterface } from "../types";
1313

14+
15+
// set OIDC response mode to query
16+
const RESPONSE_MODE = 'query';
17+
1418
/**
1519
* Send a login request to the given server, and format the response
1620
* as a MatrixClientCreds
@@ -80,10 +84,7 @@ export async function sendLoginRequest(
8084
export const getOIDCAuthUrl = async (
8185
hs: string,
8286
email: string,
83-
): Promise<{
84-
authUrl: string;
85-
responseMode: 'fragment' | 'query';
86-
}> => {
87+
): Promise<string> => {
8788
const delegatedAuthConfig = await fetchDelegatedAuthMetadata(hs);
8889
console.log('*** delegatedAuthConfig', delegatedAuthConfig);
8990
if (!delegatedAuthConfig) {
@@ -93,6 +94,7 @@ export const getOIDCAuthUrl = async (
9394
window.location.origin + window.location.pathname,
9495
);
9596
const redirectUri = urlCallback.href;
97+
console.log("*** redirectUri", redirectUri);
9698

9799
const defaultOidcClientUri = window.location.origin;
98100

@@ -108,11 +110,6 @@ export const getOIDCAuthUrl = async (
108110
});
109111

110112
const nonce = secureRandomString(10);
111-
const responseMode = delegatedAuthConfig!.response_modes_supported?.includes(
112-
'fragment',
113-
)
114-
? 'fragment'
115-
: 'query';
116113

117114
const authorizationUrl = await generateOidcAuthorizationUrl({
118115
metadata: delegatedAuthConfig!,
@@ -123,9 +120,10 @@ export const getOIDCAuthUrl = async (
123120
nonce,
124121
urlState: '',
125122
loginHint: email,
126-
responseMode
123+
responseMode: RESPONSE_MODE
127124
});
128-
return { authUrl: authorizationUrl, responseMode };
125+
console.log("*** authorizationUrl", authorizationUrl);
126+
return authorizationUrl;
129127
};
130128

131129
const fetchDelegatedAuthMetadata = async (preferredHomeserverUrl: string) => {
@@ -158,8 +156,7 @@ const fetchDelegatedAuthMetadata = async (preferredHomeserverUrl: string) => {
158156
* @throws When we failed to get a valid access token
159157
*/
160158
export const completeOidcLogin = async (
161-
params: { code: string; state: string },
162-
responseMode: 'fragment' | 'query',
159+
params: { code: string; state: string }
163160
): Promise<CompleteOidcLoginResponse> => {
164161
const { code, state } = params;
165162
const {
@@ -168,7 +165,7 @@ export const completeOidcLogin = async (
168165
idTokenClaims,
169166
identityServerUrl,
170167
oidcClientSettings,
171-
} = await completeAuthorizationCodeGrant(code, state, responseMode);
168+
} = await completeAuthorizationCodeGrant(code, state, RESPONSE_MODE);
172169

173170
return {
174171
homeserverUrl,
@@ -202,7 +199,7 @@ export const getUserIdFromAccessToken = async (
202199
idBaseUrl: identityServerUrl,
203200
});
204201

205-
return await client.whoami();
202+
return client.whoami();
206203
} catch (error) {
207204
console.error('Failed to retrieve userId using accessToken', error);
208205
throw new Error('Failed to retrieve userId using accessToken');

0 commit comments

Comments
 (0)