Add user setup config

This commit is contained in:
Joey Yakimowich-Payne 2026-01-13 16:18:46 -07:00
commit 342ff60b70
No known key found for this signature in database
GPG key ID: 6BFE655FA5ABD1E1
7 changed files with 230 additions and 18 deletions

View file

@ -43,9 +43,16 @@ export const Landing: React.FC<LandingProps> = ({ onGenerate, onCreateManual, on
};
const handleLoadQuiz = async (id: string) => {
try {
const quiz = await loadQuiz(id);
setLibraryOpen(false);
onLoadQuiz(quiz);
} catch (err) {
if (err instanceof Error && err.message.includes('redirecting')) {
return;
}
console.error('Failed to load quiz:', err);
}
};
return (

View file

@ -108,7 +108,7 @@ services:
OIDC_ISSUER: http://localhost:9000/application/o/kaboot/
OIDC_JWKS_URI: http://localhost:9000/application/o/kaboot/jwks/
OIDC_INTERNAL_JWKS_URI: http://authentik-server:9000/application/o/kaboot/jwks/
CORS_ORIGIN: ${CORS_ORIGIN:-http://localhost:3000}
CORS_ORIGIN: ${CORS_ORIGIN:-http://localhost:5173}
volumes:
- kaboot-data:/data
ports:

View file

@ -70,7 +70,168 @@ This guide walks through configuring Authentik as the OAuth2/OIDC identity provi
7. Click **Submit**
## Step 4: Verify OIDC Endpoints
## Step 4: Enable User Registration (Sign Up)
By default, Authentik only shows a login form. To allow users to sign up, you need to create an enrollment flow and link it.
### Step 4.1: Create the Enrollment Prompt Stage
1. Go to **Flows and Stages** > **Stages**
2. Click **Create**
3. Select **Prompt Stage** and click **Next**
4. Configure:
| Field | Value |
|-------|-------|
| Name | `enrollment-prompt` |
5. In the **Fields** section, move these to the **Selected** side:
- `default-source-enrollment-field-username` (username)
- `default-user-settings-field-email` (email)
- `default-password-change-field-password` (password)
- `default-password-change-field-password-repeat` (password_repeat)
6. (Optional) In **Validation policies**, select `password-complexity` if you created it in Step 4.2
7. Click **Finish**
### Step 4.2: (Optional) Create Password Complexity Policy
1. Go to **Customisation** > **Policies**
2. Click **Create** and select **Password Policy**
3. Configure:
| Field | Value |
|-------|-------|
| Name | `password-complexity` |
| Password field | `password` |
| Minimum length | `8` |
| Amount of uppercase characters | `1` |
| Amount of lowercase characters | `1` |
| Amount of digits | `1` |
4. Click **Finish**
You'll add this to the enrollment prompt stage later.
### Step 4.3: Create a Group for Kaboot Users
1. Go to **Directory** > **Groups**
2. Click **Create**
3. Configure:
| Field | Value |
|-------|-------|
| Name | `kaboot-users` |
4. Click **Create**
### Step 4.4: Create the User Write Stage
1. Go to **Flows and Stages** > **Stages**
2. Click **Create**
3. Select **User Write Stage** and click **Next**
4. Configure:
| Field | Value |
|-------|-------|
| Name | `enrollment-user-write` |
| User creation mode | `Create users when required` |
| Create users as inactive | Unchecked |
| Group | `kaboot-users` |
5. Click **Finish**
### Step 4.5: Create the User Login Stage
1. Go to **Flows and Stages** > **Stages**
2. Click **Create**
3. Select **User Login Stage** and click **Next**
4. Configure:
| Field | Value |
|-------|-------|
| Name | `enrollment-user-login` |
| Session duration | `hours=24` |
| Stay signed in offset | `days=30` |
| Network binding | `No binding` |
| GeoIP binding | `No binding` |
5. Click **Finish**
### Step 4.6: Create the Enrollment Flow
1. Go to **Flows and Stages** > **Flows**
2. Click **Create**
3. Configure:
| Field | Value |
|-------|-------|
| Name | `Enrollment Flow` |
| Title | `Sign Up` |
| Slug | `enrollment-flow` |
| Designation | `Enrollment` |
| Authentication | `No requirement` |
4. Click **Create**
5. Click on the newly created `enrollment-flow`
6. Go to the **Stage Bindings** tab
7. Click **Bind existing stage** and add stages in this order:
| Stage | Order |
|-------|-------|
| `enrollment-prompt` | 10 |
| `enrollment-user-write` | 20 |
| `enrollment-user-login` | 30 |
### Step 4.7: Bind the Group to the Kaboot Application
1. Go to **Applications** > **Applications** > **Kaboot**
2. Go to the **Policy / Group / User Bindings** tab
3. Click **Bind existing group**
4. Select `kaboot-users`
5. Click **Bind**
Now users in the `kaboot-users` group (which includes all users who sign up) will have access to Kaboot.
### Step 4.8: Link Enrollment Flow to Login
1. Go to **Flows and Stages** > **Stages**
2. Find and click on `default-authentication-identification`
3. Scroll down to **Flow settings**
4. In the **Enrollment flow** dropdown, select `enrollment-flow`
5. Click **Update**
Now when users visit the login page, they'll see a "Need an account? Sign up." link.
### Optional: Add Password Recovery
1. In **Flows and Stages** > **Stages** > `default-authentication-identification`
2. Set **Recovery flow** to `default-recovery-flow` (if it exists)
3. Click **Update**
## Step 5: Verify OIDC Endpoints
After creation, go to **Applications** > **Providers** > **Kaboot OAuth2**

View file

@ -6,35 +6,70 @@ const API_URL = import.meta.env.VITE_API_URL || 'http://localhost:3001';
export const useAuthenticatedFetch = () => {
const auth = useAuth();
const authFetch = useCallback(
async (path: string, options: RequestInit = {}): Promise<Response> => {
const isTokenExpired = useCallback(() => {
if (!auth.user?.expires_at) return true;
const expiresAt = auth.user.expires_at * 1000;
const now = Date.now();
const bufferMs = 60 * 1000;
return now >= expiresAt - bufferMs;
}, [auth.user?.expires_at]);
const ensureValidToken = useCallback(async (): Promise<string> => {
if (!auth.user?.access_token) {
throw new Error('Not authenticated');
}
if (isTokenExpired()) {
try {
const user = await auth.signinSilent();
if (user?.access_token) {
return user.access_token;
}
} catch {
auth.signinRedirect();
throw new Error('Session expired, redirecting to login');
}
}
return auth.user.access_token;
}, [auth, isTokenExpired]);
const authFetch = useCallback(
async (path: string, options: RequestInit = {}): Promise<Response> => {
const token = await ensureValidToken();
const url = path.startsWith('http') ? path : `${API_URL}${path}`;
const response = await fetch(url, {
...options,
headers: {
...options.headers,
Authorization: `Bearer ${auth.user.access_token}`,
Authorization: `Bearer ${token}`,
'Content-Type': 'application/json',
},
});
if (response.status === 401) {
try {
await auth.signinSilent();
const user = await auth.signinSilent();
if (user?.access_token) {
return fetch(url, {
...options,
headers: {
...options.headers,
Authorization: `Bearer ${user.access_token}`,
'Content-Type': 'application/json',
},
});
}
} catch {
auth.signinRedirect();
}
throw new Error('Token expired, please retry');
throw new Error('Session expired, redirecting to login');
}
return response;
},
[auth]
[auth, ensureValidToken]
);
return {

View file

@ -9,10 +9,20 @@ if (!rootElement) {
throw new Error("Could not find root element to mount to");
}
const onSigninCallback = () => {
window.history.replaceState({}, document.title, window.location.pathname);
};
const root = ReactDOM.createRoot(rootElement);
root.render(
<React.StrictMode>
<AuthProvider {...oidcConfig}>
<AuthProvider
{...oidcConfig}
onSigninCallback={onSigninCallback}
onRemoveUser={() => {
window.localStorage.clear();
}}
>
<App />
</AuthProvider>
</React.StrictMode>

View file

@ -12,9 +12,8 @@ export const oidcConfig = {
response_type: 'code',
scope: 'openid profile email offline_access',
automaticSilentRenew: true,
silentRequestTimeoutInSeconds: 10,
loadUserInfo: true,
userStore: new WebStorageStateStore({ store: window.localStorage }),
onSigninCallback: () => {
window.history.replaceState({}, document.title, window.location.pathname);
},
monitorSession: false,
};

View file

@ -6,7 +6,7 @@ export default defineConfig(({ mode }) => {
const env = loadEnv(mode, '.', '');
return {
server: {
port: 3000,
port: 5173,
host: '0.0.0.0',
},
plugins: [react()],