auth fixes
This commit is contained in:
@@ -0,0 +1 @@
|
|||||||
|
3.10.18
|
||||||
@@ -12,21 +12,25 @@ bearer_scheme = HTTPBearer(auto_error=False)
|
|||||||
|
|
||||||
|
|
||||||
async def _get_or_create_dev_user(session: AsyncSession) -> User:
|
async def _get_or_create_dev_user(session: AsyncSession) -> User:
|
||||||
"""Return or create the fixed development user."""
|
|
||||||
result = await session.execute(
|
result = await session.execute(
|
||||||
select(User).where(User.authentik_sub == "dev-user")
|
select(User).where(User.authentik_sub == "dev-user")
|
||||||
)
|
)
|
||||||
user = result.scalar_one_or_none()
|
user = result.scalar_one_or_none()
|
||||||
if user is None:
|
if user is None:
|
||||||
user = User(
|
result = await session.execute(
|
||||||
authentik_sub="dev-user",
|
select(User).where(User.email == "dev@localhost")
|
||||||
email="dev@localhost",
|
|
||||||
display_name="Dev User",
|
|
||||||
is_active=True,
|
|
||||||
)
|
)
|
||||||
session.add(user)
|
user = result.scalar_one_or_none()
|
||||||
await session.commit()
|
if user is None:
|
||||||
await session.refresh(user)
|
user = User(
|
||||||
|
authentik_sub="dev-user",
|
||||||
|
email="dev@localhost",
|
||||||
|
display_name="Dev User",
|
||||||
|
is_active=True,
|
||||||
|
)
|
||||||
|
session.add(user)
|
||||||
|
await session.commit()
|
||||||
|
await session.refresh(user)
|
||||||
return user
|
return user
|
||||||
|
|
||||||
|
|
||||||
@@ -44,7 +48,7 @@ async def get_current_user(
|
|||||||
)
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
claims = decode_token(token.credentials)
|
claims = await decode_token(token.credentials)
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||||
@@ -69,6 +73,16 @@ async def get_current_user(
|
|||||||
user = result.scalar_one_or_none()
|
user = result.scalar_one_or_none()
|
||||||
|
|
||||||
if user is None:
|
if user is None:
|
||||||
|
result = await session.execute(
|
||||||
|
select(User).where(User.email == email)
|
||||||
|
)
|
||||||
|
existing_user = result.scalar_one_or_none()
|
||||||
|
if existing_user:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_409_CONFLICT,
|
||||||
|
detail=f"User with email {email} already exists",
|
||||||
|
)
|
||||||
|
|
||||||
user = User(
|
user = User(
|
||||||
authentik_sub=authentik_sub,
|
authentik_sub=authentik_sub,
|
||||||
email=email,
|
email=email,
|
||||||
@@ -105,7 +119,7 @@ async def validate_traefik_auth(
|
|||||||
)
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
claims = decode_token(token.credentials)
|
claims = await decode_token(token.credentials)
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||||
|
|||||||
+24
-19
@@ -1,31 +1,17 @@
|
|||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
|
import httpx
|
||||||
import jwt
|
import jwt
|
||||||
|
|
||||||
from app.config import settings
|
from app.config import settings
|
||||||
|
|
||||||
|
_jwks_cache: dict[str, Any] | None = None
|
||||||
|
|
||||||
def decode_token(token: str) -> dict[str, Any]:
|
|
||||||
"""Decode a JWT token.
|
|
||||||
|
|
||||||
When authentik_issuer_url is configured, validates the token
|
async def decode_token(token: str) -> dict[str, Any]:
|
||||||
against the OIDC discovery document JWKS.
|
|
||||||
Otherwise, decodes without verification (local development only).
|
|
||||||
"""
|
|
||||||
if settings.authentik_issuer_url:
|
if settings.authentik_issuer_url:
|
||||||
import httpx
|
|
||||||
|
|
||||||
issuer = settings.authentik_issuer_url.rstrip("/")
|
issuer = settings.authentik_issuer_url.rstrip("/")
|
||||||
discovery_url = f"{issuer}/.well-known/openid-configuration"
|
jwks = await _get_jwks(issuer)
|
||||||
with httpx.Client() as client:
|
|
||||||
resp = client.get(discovery_url)
|
|
||||||
resp.raise_for_status()
|
|
||||||
discovery = resp.json()
|
|
||||||
jwks_uri = discovery["jwks_uri"]
|
|
||||||
|
|
||||||
jwks_resp = client.get(jwks_uri)
|
|
||||||
jwks_resp.raise_for_status()
|
|
||||||
jwks = jwks_resp.json()
|
|
||||||
|
|
||||||
signing_key = jwt.algorithms.RSAAlgorithm.from_jwk(
|
signing_key = jwt.algorithms.RSAAlgorithm.from_jwk(
|
||||||
_find_matching_key(jwks, token)
|
_find_matching_key(jwks, token)
|
||||||
@@ -42,8 +28,27 @@ def decode_token(token: str) -> dict[str, Any]:
|
|||||||
return jwt.decode(token, options={"verify_signature": False})
|
return jwt.decode(token, options={"verify_signature": False})
|
||||||
|
|
||||||
|
|
||||||
|
async def _get_jwks(issuer: str) -> dict[str, Any]:
|
||||||
|
global _jwks_cache
|
||||||
|
|
||||||
|
if _jwks_cache is not None:
|
||||||
|
return _jwks_cache
|
||||||
|
|
||||||
|
discovery_url = f"{issuer}/.well-known/openid-configuration"
|
||||||
|
async with httpx.AsyncClient() as client:
|
||||||
|
resp = await client.get(discovery_url)
|
||||||
|
resp.raise_for_status()
|
||||||
|
discovery = resp.json()
|
||||||
|
jwks_uri = discovery["jwks_uri"]
|
||||||
|
|
||||||
|
jwks_resp = await client.get(jwks_uri)
|
||||||
|
jwks_resp.raise_for_status()
|
||||||
|
_jwks_cache = jwks_resp.json()
|
||||||
|
|
||||||
|
return _jwks_cache
|
||||||
|
|
||||||
|
|
||||||
def _find_matching_key(jwks: dict[str, Any], token: str) -> dict[str, Any]:
|
def _find_matching_key(jwks: dict[str, Any], token: str) -> dict[str, Any]:
|
||||||
"""Find the key in JWKS that matches the token's kid header."""
|
|
||||||
unverified_header = jwt.get_unverified_header(token)
|
unverified_header = jwt.get_unverified_header(token)
|
||||||
kid = unverified_header.get("kid")
|
kid = unverified_header.get("kid")
|
||||||
for key in jwks.get("keys", []):
|
for key in jwks.get("keys", []):
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ class Settings(BaseSettings):
|
|||||||
database_url: str = "postgresql://postgres:postgres@localhost:5432/headquarter"
|
database_url: str = "postgresql://postgres:postgres@localhost:5432/headquarter"
|
||||||
|
|
||||||
# CORS
|
# CORS
|
||||||
cors_origins: str = "http://localhost:5173"
|
cors_origins: str = "http://localhost:5173,http://localhost:3000"
|
||||||
|
|
||||||
# Deployment
|
# Deployment
|
||||||
root_domain: str = "localhost"
|
root_domain: str = "localhost"
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ app = FastAPI(
|
|||||||
lifespan=lifespan,
|
lifespan=lifespan,
|
||||||
)
|
)
|
||||||
|
|
||||||
allow_origins = ["*"] if settings.debug else []
|
allow_origins = settings.cors_origins.split(",") if settings.cors_origins else []
|
||||||
app.add_middleware(
|
app.add_middleware(
|
||||||
CORSMiddleware,
|
CORSMiddleware,
|
||||||
allow_origins=allow_origins,
|
allow_origins=allow_origins,
|
||||||
|
|||||||
@@ -64,3 +64,24 @@ async def test_inactive_user_raises_403(
|
|||||||
await session.commit()
|
await session.commit()
|
||||||
|
|
||||||
assert response.status_code == 403
|
assert response.status_code == 403
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_dev_bypass_email_uniqueness(
|
||||||
|
auth_client: AsyncClient, db_session: async_sessionmaker[Any]
|
||||||
|
) -> None:
|
||||||
|
async with db_session() as session:
|
||||||
|
result = await session.execute(select(User).where(User.email == "dev@localhost"))
|
||||||
|
existing = result.scalar_one_or_none()
|
||||||
|
if existing:
|
||||||
|
await session.delete(existing)
|
||||||
|
await session.commit()
|
||||||
|
|
||||||
|
response = await auth_client.get("/api/v1/users/me")
|
||||||
|
assert response.status_code == 200
|
||||||
|
|
||||||
|
async with db_session() as session:
|
||||||
|
result = await session.execute(select(User).where(User.email == "dev@localhost"))
|
||||||
|
user = result.scalar_one_or_none()
|
||||||
|
assert user is not None
|
||||||
|
assert user.authentik_sub == "dev-user"
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import type { Config, ConfigCreate, Project, ProjectCreate, ProjectUpdate, Repository, RepositoryConnection, RepositoryConnectionCreate, RepositoryCreate, Secret, SecretCreate, ToolDefinition, ToolInstance, User } from '../types/api.ts'
|
import type { Config, ConfigCreate, Project, ProjectCreate, ProjectUpdate, Repository, RepositoryConnection, RepositoryConnectionCreate, RepositoryCreate, Secret, SecretCreate, ToolDefinition, ToolInstance, User } from '../types/api.ts'
|
||||||
|
import { getToken, clearToken } from '../auth/oidc'
|
||||||
|
|
||||||
const API_URL = import.meta.env.VITE_API_URL || 'http://localhost:8000'
|
const API_URL = import.meta.env.VITE_API_URL || 'http://localhost:8000'
|
||||||
|
|
||||||
@@ -13,8 +14,49 @@ export class ApiError extends Error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function getAuthToken(): string | null {
|
let isRefreshing = false
|
||||||
return localStorage.getItem('access_token')
|
let refreshPromise: Promise<string | null> | null = null
|
||||||
|
|
||||||
|
async function refreshToken(): Promise<string | null> {
|
||||||
|
if (isRefreshing && refreshPromise) {
|
||||||
|
return refreshPromise
|
||||||
|
}
|
||||||
|
|
||||||
|
isRefreshing = true
|
||||||
|
refreshPromise = (async () => {
|
||||||
|
try {
|
||||||
|
const token = getToken()
|
||||||
|
if (!token) return null
|
||||||
|
|
||||||
|
const response = await fetch(`${API_URL}/api/v1/auth/refresh`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Authorization': `Bearer ${token}`,
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
clearToken()
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json()
|
||||||
|
if (data.access_token) {
|
||||||
|
localStorage.setItem('access_token', data.access_token)
|
||||||
|
return data.access_token
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
} catch {
|
||||||
|
clearToken()
|
||||||
|
return null
|
||||||
|
} finally {
|
||||||
|
isRefreshing = false
|
||||||
|
refreshPromise = null
|
||||||
|
}
|
||||||
|
})()
|
||||||
|
|
||||||
|
return refreshPromise
|
||||||
}
|
}
|
||||||
|
|
||||||
async function fetchWithAuth(
|
async function fetchWithAuth(
|
||||||
@@ -22,7 +64,7 @@ async function fetchWithAuth(
|
|||||||
options: RequestInit = {}
|
options: RequestInit = {}
|
||||||
): Promise<Response> {
|
): Promise<Response> {
|
||||||
const url = `${API_URL}/api/v1${endpoint}`
|
const url = `${API_URL}/api/v1${endpoint}`
|
||||||
const token = getAuthToken()
|
const token = getToken()
|
||||||
|
|
||||||
const headers: Record<string, string> = {
|
const headers: Record<string, string> = {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
@@ -46,6 +88,22 @@ async function fetchWithAuth(
|
|||||||
console.log(`[API] ${response.status} ${response.statusText}`)
|
console.log(`[API] ${response.status} ${response.statusText}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (response.status === 401 && token) {
|
||||||
|
const newToken = await refreshToken()
|
||||||
|
if (newToken) {
|
||||||
|
headers['Authorization'] = `Bearer ${newToken}`
|
||||||
|
const retryResponse = await fetch(url, {
|
||||||
|
...options,
|
||||||
|
headers,
|
||||||
|
})
|
||||||
|
if (!retryResponse.ok) {
|
||||||
|
const data = await retryResponse.json().catch(() => undefined)
|
||||||
|
throw new ApiError(retryResponse.status, retryResponse.statusText, data)
|
||||||
|
}
|
||||||
|
return retryResponse
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
const data = await response.json().catch(() => undefined)
|
const data = await response.json().catch(() => undefined)
|
||||||
throw new ApiError(response.status, response.statusText, data)
|
throw new ApiError(response.status, response.statusText, data)
|
||||||
|
|||||||
@@ -1,25 +1,45 @@
|
|||||||
import { useEffect } from 'react'
|
import { useEffect, useRef } from 'react'
|
||||||
import { useAuthStore } from '../stores/auth'
|
import { useAuthStore } from '../stores/auth'
|
||||||
import { api } from '../api/client'
|
import { api } from '../api/client'
|
||||||
|
import { getToken, clearToken } from './oidc'
|
||||||
|
|
||||||
export function AuthProvider({ children }: { children: React.ReactNode }) {
|
export function AuthProvider({ children }: { children: React.ReactNode }) {
|
||||||
const { setUser, setError, setState } = useAuthStore()
|
const { setUser, setError, setState } = useAuthStore()
|
||||||
|
const isMounted = useRef(true)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
isMounted.current = false
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const initAuth = async () => {
|
const initAuth = async () => {
|
||||||
try {
|
try {
|
||||||
const token = localStorage.getItem('access_token')
|
const token = getToken()
|
||||||
if (!token) {
|
if (!token) {
|
||||||
setState('unauthenticated')
|
if (isMounted.current) {
|
||||||
|
setState('unauthenticated')
|
||||||
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const user = await api.getCurrentUser()
|
const user = await api.getCurrentUser()
|
||||||
setUser(user)
|
|
||||||
|
if (isMounted.current) {
|
||||||
|
setUser(user)
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Auth initialization failed:', error)
|
console.error('Auth initialization failed:', error)
|
||||||
setError(error instanceof Error ? error : new Error('Auth failed'))
|
|
||||||
localStorage.removeItem('access_token')
|
if (error instanceof Error && error.message.includes('401')) {
|
||||||
|
clearToken()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isMounted.current) {
|
||||||
|
setError(error instanceof Error ? error : new Error('Auth failed'))
|
||||||
|
clearToken()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
// PKCE utilities for OIDC flow
|
|
||||||
function generateRandomString(length: number): string {
|
function generateRandomString(length: number): string {
|
||||||
const possible = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'
|
const possible = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'
|
||||||
let text = ''
|
let text = ''
|
||||||
@@ -34,6 +33,18 @@ export function clearPKCE(): void {
|
|||||||
sessionStorage.removeItem('pkce_verifier')
|
sessionStorage.removeItem('pkce_verifier')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function storeState(state: string): void {
|
||||||
|
sessionStorage.setItem('oidc_state', state)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getState(): string | null {
|
||||||
|
return sessionStorage.getItem('oidc_state')
|
||||||
|
}
|
||||||
|
|
||||||
|
export function clearState(): void {
|
||||||
|
sessionStorage.removeItem('oidc_state')
|
||||||
|
}
|
||||||
|
|
||||||
export function storeToken(token: string): void {
|
export function storeToken(token: string): void {
|
||||||
localStorage.setItem('access_token', token)
|
localStorage.setItem('access_token', token)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useRef, useState } from 'react'
|
||||||
import { useNavigate } from 'react-router-dom'
|
import { useNavigate } from 'react-router-dom'
|
||||||
import { getPKCE, clearPKCE, storeToken } from '../auth/oidc'
|
import { getPKCE, clearPKCE, storeToken, getState, clearState } from '../auth/oidc'
|
||||||
import { api } from '../api/client'
|
import { api } from '../api/client'
|
||||||
import { useAuthStore } from '../stores/auth'
|
import { useAuthStore } from '../stores/auth'
|
||||||
|
|
||||||
const OIDC_ISSUER = import.meta.env.VITE_OIDC_ISSUER
|
|
||||||
const CLIENT_ID = import.meta.env.VITE_OIDC_CLIENT_ID
|
const CLIENT_ID = import.meta.env.VITE_OIDC_CLIENT_ID
|
||||||
const REDIRECT_URI = import.meta.env.VITE_OIDC_REDIRECT_URI
|
const REDIRECT_URI = import.meta.env.VITE_OIDC_REDIRECT_URI
|
||||||
|
|
||||||
@@ -12,6 +11,13 @@ export default function CallbackPage() {
|
|||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const { setUser, setError } = useAuthStore()
|
const { setUser, setError } = useAuthStore()
|
||||||
const [status, setStatus] = useState('Processing authentication...')
|
const [status, setStatus] = useState('Processing authentication...')
|
||||||
|
const isMounted = useRef(true)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
isMounted.current = false
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleCallback = async () => {
|
const handleCallback = async () => {
|
||||||
@@ -19,6 +25,8 @@ export default function CallbackPage() {
|
|||||||
const urlParams = new URLSearchParams(window.location.search)
|
const urlParams = new URLSearchParams(window.location.search)
|
||||||
const code = urlParams.get('code')
|
const code = urlParams.get('code')
|
||||||
const error = urlParams.get('error')
|
const error = urlParams.get('error')
|
||||||
|
const state = urlParams.get('state')
|
||||||
|
const storedState = getState()
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
throw new Error(`Authentication error: ${error}`)
|
throw new Error(`Authentication error: ${error}`)
|
||||||
@@ -28,12 +36,18 @@ export default function CallbackPage() {
|
|||||||
throw new Error('No authorization code received')
|
throw new Error('No authorization code received')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!state || state !== storedState) {
|
||||||
|
throw new Error('Invalid or missing state parameter')
|
||||||
|
}
|
||||||
|
|
||||||
const verifier = getPKCE()
|
const verifier = getPKCE()
|
||||||
if (!verifier) {
|
if (!verifier) {
|
||||||
throw new Error('PKCE verifier not found')
|
throw new Error('PKCE verifier not found')
|
||||||
}
|
}
|
||||||
|
|
||||||
setStatus('Exchanging code for token...')
|
if (isMounted.current) {
|
||||||
|
setStatus('Exchanging code for token...')
|
||||||
|
}
|
||||||
|
|
||||||
const tokenEndpoint = 'https://auth.commumedia.org/application/o/token/'
|
const tokenEndpoint = 'https://auth.commumedia.org/application/o/token/'
|
||||||
const tokenResponse = await fetch(tokenEndpoint, {
|
const tokenResponse = await fetch(tokenEndpoint, {
|
||||||
@@ -51,23 +65,35 @@ export default function CallbackPage() {
|
|||||||
})
|
})
|
||||||
|
|
||||||
if (!tokenResponse.ok) {
|
if (!tokenResponse.ok) {
|
||||||
throw new Error('Token exchange failed')
|
const errorData = await tokenResponse.json().catch(() => ({}))
|
||||||
|
throw new Error(errorData.error_description || errorData.error || 'Token exchange failed')
|
||||||
}
|
}
|
||||||
|
|
||||||
const tokenData = await tokenResponse.json()
|
const tokenData = await tokenResponse.json()
|
||||||
storeToken(tokenData.access_token)
|
storeToken(tokenData.access_token)
|
||||||
clearPKCE()
|
clearPKCE()
|
||||||
|
clearState()
|
||||||
|
|
||||||
|
if (isMounted.current) {
|
||||||
|
setStatus('Fetching user information...')
|
||||||
|
}
|
||||||
|
|
||||||
setStatus('Fetching user information...')
|
|
||||||
const user = await api.getCurrentUser()
|
const user = await api.getCurrentUser()
|
||||||
setUser(user)
|
|
||||||
|
|
||||||
navigate('/')
|
if (isMounted.current) {
|
||||||
|
setUser(user)
|
||||||
|
window.location.href = '/'
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Callback error:', error)
|
console.error('Callback error:', error)
|
||||||
setError(error instanceof Error ? error : new Error('Authentication failed'))
|
clearPKCE()
|
||||||
setStatus('Authentication failed')
|
clearState()
|
||||||
setTimeout(() => navigate('/login'), 3000)
|
|
||||||
|
if (isMounted.current) {
|
||||||
|
setError(error instanceof Error ? error : new Error('Authentication failed'))
|
||||||
|
setStatus('Authentication failed')
|
||||||
|
setTimeout(() => navigate('/login'), 3000)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,16 +1,24 @@
|
|||||||
import { useEffect } from 'react'
|
import { useEffect } from 'react'
|
||||||
import { createPKCE, storePKCE } from '../auth/oidc'
|
import { createPKCE, storePKCE, storeState } from '../auth/oidc'
|
||||||
|
|
||||||
const OIDC_ISSUER = import.meta.env.VITE_OIDC_ISSUER
|
|
||||||
const CLIENT_ID = import.meta.env.VITE_OIDC_CLIENT_ID
|
const CLIENT_ID = import.meta.env.VITE_OIDC_CLIENT_ID
|
||||||
const REDIRECT_URI = import.meta.env.VITE_OIDC_REDIRECT_URI
|
const REDIRECT_URI = import.meta.env.VITE_OIDC_REDIRECT_URI
|
||||||
|
|
||||||
|
function generateState(): string {
|
||||||
|
const array = new Uint8Array(32)
|
||||||
|
crypto.getRandomValues(array)
|
||||||
|
return Array.from(array, byte => byte.toString(16).padStart(2, '0')).join('')
|
||||||
|
}
|
||||||
|
|
||||||
export default function LoginPage() {
|
export default function LoginPage() {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const initiateLogin = async () => {
|
const initiateLogin = async () => {
|
||||||
const { verifier, challenge } = await createPKCE()
|
const { verifier, challenge } = await createPKCE()
|
||||||
storePKCE(verifier)
|
storePKCE(verifier)
|
||||||
|
|
||||||
|
const state = generateState()
|
||||||
|
storeState(state)
|
||||||
|
|
||||||
const params = new URLSearchParams({
|
const params = new URLSearchParams({
|
||||||
client_id: CLIENT_ID,
|
client_id: CLIENT_ID,
|
||||||
redirect_uri: REDIRECT_URI,
|
redirect_uri: REDIRECT_URI,
|
||||||
@@ -18,6 +26,7 @@ export default function LoginPage() {
|
|||||||
scope: 'openid profile email',
|
scope: 'openid profile email',
|
||||||
code_challenge: challenge,
|
code_challenge: challenge,
|
||||||
code_challenge_method: 'S256',
|
code_challenge_method: 'S256',
|
||||||
|
state,
|
||||||
})
|
})
|
||||||
|
|
||||||
const authorizationEndpoint = 'https://auth.commumedia.org/application/o/authorize/'
|
const authorizationEndpoint = 'https://auth.commumedia.org/application/o/authorize/'
|
||||||
|
|||||||
@@ -20,5 +20,8 @@ export const useAuthStore = create<AuthStore>((set) => ({
|
|||||||
setState: (state) => set({ state }),
|
setState: (state) => set({ state }),
|
||||||
setUser: (user) => set({ user, state: user ? 'authenticated' : 'unauthenticated' }),
|
setUser: (user) => set({ user, state: user ? 'authenticated' : 'unauthenticated' }),
|
||||||
setError: (error) => set({ error, state: 'error' }),
|
setError: (error) => set({ error, state: 'error' }),
|
||||||
logout: () => set({ user: null, state: 'unauthenticated', error: null }),
|
logout: () => {
|
||||||
|
localStorage.removeItem('access_token')
|
||||||
|
set({ user: null, state: 'unauthenticated', error: null })
|
||||||
|
},
|
||||||
}))
|
}))
|
||||||
|
|||||||
Reference in New Issue
Block a user