This repository has no description
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666// SPDX-License-Identifier: AGPL-3.0-or-later
import {requireClientIp} from '@fluxer/ip_utils/src/ClientIp';import { AuthLoginResponse, AuthorizeIpRequest, AuthRegisterResponse, AuthSessionsResponse, AuthTokenWithUserIdResponse, EmailRevertRequest, ForgotPasswordRequest, HandoffCodeParam, HandoffCompleteRequest, HandoffInfoResponse, HandoffInitiateResponse, HandoffStatusResponse, IpAuthorizationPollQuery, IpAuthorizationPollResponse, LoginRequest, LogoutAuthSessionsRequest, MfaTicketRequest, MfaTotpRequest, RegisterRequest, ResetPasswordRequest, ResetPasswordTokenParam, SsoCompleteRequest, SsoCompleteResponse, SsoStartRequest, SsoStartResponse, SsoStatusResponse, SudoVerificationSchema, UsernameSuggestionsRequest, UsernameSuggestionsResponse, ValidateResetPasswordTokenResponse, VerifyEmailRequest, WebAuthnAuthenticateRequest, WebAuthnAuthenticationOptionsResponse, WebAuthnMfaRequest,} from '@fluxer/schema/src/domains/auth/AuthSchemas';import {Config} from '../Config';import {DefaultUserOnly, LoginRequiredAllowSuspicious} from '../middleware/AuthMiddleware';import {CaptchaMiddleware, CaptchaMiddlewareSkipFlutter} from '../middleware/CaptchaMiddleware';import {LocalAuthMiddleware} from '../middleware/LocalAuthMiddleware';import {RateLimitMiddleware} from '../middleware/RateLimitMiddleware';import {OpenAPI} from '../middleware/ResponseTypeMiddleware';import {SudoModeMiddleware} from '../middleware/SudoModeMiddleware';import {RateLimitConfigs} from '../RateLimitConfig';import type {HonoApp} from '../types/HonoEnv';import {Validator} from '../Validator';import {requireSudoMode} from './services/SudoVerificationService';
export function AuthController(app: HonoApp) { app.get( '/auth/sso/status', RateLimitMiddleware(RateLimitConfigs.AUTH_SSO_START), OpenAPI({ operationId: 'get_sso_status', summary: 'Get SSO status', responseSchema: SsoStatusResponse, statusCode: 200, security: [], tags: ['Auth'], description: 'Retrieve the current status of the SSO authentication session without authentication required.', }), async (ctx) => { const status = await ctx.get('authRequestService').getSsoStatus(); return ctx.json(status); }, ); app.post( '/auth/sso/start', RateLimitMiddleware(RateLimitConfigs.AUTH_SSO_START), Validator('json', SsoStartRequest), OpenAPI({ operationId: 'start_sso', summary: 'Start SSO', responseSchema: SsoStartResponse, statusCode: 200, security: [], tags: ['Auth'], description: 'Initiate a new Single Sign-On (SSO) session. Returns a session URL to be completed with SSO provider credentials.', }), async (ctx) => { const result = await ctx.get('authRequestService').startSso(ctx.req.valid('json')); return ctx.json(result); }, ); app.post( '/auth/sso/complete', RateLimitMiddleware(RateLimitConfigs.AUTH_SSO_COMPLETE), Validator('json', SsoCompleteRequest), OpenAPI({ operationId: 'complete_sso', summary: 'Complete SSO', responseSchema: SsoCompleteResponse, statusCode: 200, security: [], tags: ['Auth'], description: 'Complete the SSO authentication flow with the authorization code from the SSO provider. Returns authentication token and user information.', }), async (ctx) => { const result = await ctx.get('authRequestService').completeSso(ctx.req.valid('json'), ctx.req.raw); return ctx.json(result); }, ); app.post( '/auth/register', LocalAuthMiddleware, CaptchaMiddleware, RateLimitMiddleware(RateLimitConfigs.AUTH_REGISTER), Validator('json', RegisterRequest), OpenAPI({ operationId: 'register_account', summary: 'Register account', responseSchema: AuthRegisterResponse, statusCode: 200, security: [], tags: ['Auth'], description: 'Create a new user account with email and password. Requires CAPTCHA verification. User account is created but must verify email before logging in.', }), async (ctx) => { const result = await ctx.get('authRequestService').register({ data: ctx.req.valid('json'), request: ctx.req.raw, requestCache: ctx.get('requestCache'), }); return ctx.json(result); }, ); app.post( '/auth/login', LocalAuthMiddleware, CaptchaMiddlewareSkipFlutter, RateLimitMiddleware(RateLimitConfigs.AUTH_LOGIN), Validator('json', LoginRequest), OpenAPI({ operationId: 'login_user', summary: 'Login account', responseSchema: AuthLoginResponse, statusCode: 200, security: [], tags: ['Auth'], description: 'Authenticate with email and password. Returns authentication token if credentials are valid and MFA is not required. If MFA is enabled, returns a ticket for MFA verification.', }), async (ctx) => { const result = await ctx.get('authRequestService').login({ data: ctx.req.valid('json'), request: ctx.req.raw, requestCache: ctx.get('requestCache'), }); return ctx.json(result); }, ); app.post( '/auth/login/mfa/totp', LocalAuthMiddleware, RateLimitMiddleware(RateLimitConfigs.AUTH_LOGIN_MFA), Validator('json', MfaTotpRequest), OpenAPI({ operationId: 'login_with_totp', summary: 'Login with TOTP', responseSchema: AuthTokenWithUserIdResponse, statusCode: 200, security: [], tags: ['Auth'], description: 'Complete login by verifying TOTP code during multi-factor authentication. Requires the MFA ticket from initial login attempt.', }), async (ctx) => { const {code, ticket} = ctx.req.valid('json'); const result = await ctx.get('authRequestService').loginMfaTotp({code, ticket, request: ctx.req.raw}); return ctx.json(result); }, ); app.post( '/auth/logout', RateLimitMiddleware(RateLimitConfigs.AUTH_LOGOUT), OpenAPI({ operationId: 'logout_user', summary: 'Logout account', responseSchema: null, statusCode: 204, security: ['bearerToken', 'sessionToken'], tags: ['Auth'], description: 'Invalidate the current authentication token and end the session. The auth token in the Authorization header will no longer be valid.', }), async (ctx) => { await ctx.get('authRequestService').logout({ authorizationHeader: ctx.req.header('Authorization') ?? undefined, authToken: ctx.get('authToken') ?? undefined, }); return ctx.body(null, 204); }, ); app.post( '/auth/verify', LocalAuthMiddleware, RateLimitMiddleware(RateLimitConfigs.AUTH_VERIFY_EMAIL), Validator('json', VerifyEmailRequest), OpenAPI({ operationId: 'verify_email', summary: 'Verify email', responseSchema: null, statusCode: 204, security: [], tags: ['Auth'], description: 'Verify user email address using the code sent during registration. Email verification is required before the account becomes fully usable.', }), async (ctx) => { await ctx.get('authRequestService').verifyEmail(ctx.req.valid('json')); return ctx.body(null, 204); }, ); app.post( '/auth/verify/resend', LocalAuthMiddleware, RateLimitMiddleware(RateLimitConfigs.AUTH_RESEND_VERIFICATION), LoginRequiredAllowSuspicious, DefaultUserOnly, OpenAPI({ operationId: 'resend_verification_email', summary: 'Resend verification email', responseSchema: null, statusCode: 204, security: ['bearerToken', 'sessionToken'], tags: ['Auth'], description: 'Request a new email verification code to be sent. Requires authentication. Use this if the original verification email was lost or expired.', }), async (ctx) => { await ctx.get('authRequestService').resendVerificationEmail(ctx.get('user')); return ctx.body(null, 204); }, ); app.post( '/auth/forgot', LocalAuthMiddleware, CaptchaMiddleware, RateLimitMiddleware(RateLimitConfigs.AUTH_FORGOT_PASSWORD), Validator('json', ForgotPasswordRequest), OpenAPI({ operationId: 'forgot_password', summary: 'Forgot password', responseSchema: null, statusCode: 204, security: [], tags: ['Auth'], description: "Initiate password reset process by email. A password reset link will be sent to the user's email address. Requires CAPTCHA verification.", }), async (ctx) => { await ctx.get('authRequestService').forgotPassword({ data: ctx.req.valid('json'), request: ctx.req.raw, }); return ctx.body(null, 204); }, ); app.get( '/auth/reset/:token', LocalAuthMiddleware, RateLimitMiddleware(RateLimitConfigs.AUTH_VALIDATE_RESET_TOKEN), Validator('param', ResetPasswordTokenParam), OpenAPI({ operationId: 'validate_reset_password_token', summary: 'Validate reset password token', responseSchema: ValidateResetPasswordTokenResponse, statusCode: 200, security: [], tags: ['Auth'], description: 'Check whether a password reset token is valid and unexpired before allowing the user to submit a new password. Does not consume the token.', }), async (ctx) => { const result = await ctx.get('authRequestService').validateResetPasswordToken(ctx.req.valid('param').token); return ctx.json(result); }, ); app.post( '/auth/reset', LocalAuthMiddleware, RateLimitMiddleware(RateLimitConfigs.AUTH_RESET_PASSWORD), Validator('json', ResetPasswordRequest), OpenAPI({ operationId: 'reset_password', summary: 'Reset password', responseSchema: AuthLoginResponse, statusCode: 200, security: [], tags: ['Auth'], description: 'Complete the password reset flow using the token from the reset email. Returns authentication token after successful password reset.', }), async (ctx) => { const result = await ctx.get('authRequestService').resetPassword({ data: ctx.req.valid('json'), request: ctx.req.raw, }); return ctx.json(result); }, ); app.post( '/auth/email-revert', LocalAuthMiddleware, RateLimitMiddleware(RateLimitConfigs.AUTH_EMAIL_REVERT), Validator('json', EmailRevertRequest), OpenAPI({ operationId: 'revert_email_change', summary: 'Revert email change', responseSchema: AuthLoginResponse, statusCode: 200, security: [], tags: ['Auth'], description: 'Revert a pending email change using the verification token sent to the old email. Returns authentication token after successful revert.', }), async (ctx) => { const result = await ctx.get('authRequestService').revertEmailChange({ data: ctx.req.valid('json'), request: ctx.req.raw, }); return ctx.json(result); }, ); app.get( '/auth/sessions', RateLimitMiddleware(RateLimitConfigs.AUTH_SESSIONS_GET), LoginRequiredAllowSuspicious, DefaultUserOnly, OpenAPI({ operationId: 'list_auth_sessions', summary: 'List auth sessions', responseSchema: AuthSessionsResponse, statusCode: 200, security: ['bearerToken', 'sessionToken'], tags: ['Auth'], description: 'Retrieve all active authentication sessions for the current user. Requires authentication.', }), async (ctx) => { const userId = ctx.get('user').id; return ctx.json(await ctx.get('authRequestService').getAuthSessions(userId)); }, ); app.post( '/auth/sessions/logout', RateLimitMiddleware(RateLimitConfigs.AUTH_SESSIONS_LOGOUT), LoginRequiredAllowSuspicious, DefaultUserOnly, SudoModeMiddleware, Validator('json', LogoutAuthSessionsRequest.merge(SudoVerificationSchema)), OpenAPI({ operationId: 'logout_all_sessions', summary: 'Logout all sessions', responseSchema: null, statusCode: 204, security: ['bearerToken', 'sessionToken'], tags: ['Auth'], description: 'Invalidate all active authentication sessions for the current user. Requires sudo mode verification for security.', }), async (ctx) => { const user = ctx.get('user'); const body = ctx.req.valid('json'); await requireSudoMode(ctx, user, body); await ctx.get('authRequestService').logoutAuthSessions({user, data: body}); return ctx.body(null, 204); }, ); app.post( '/auth/authorize-ip', LocalAuthMiddleware, RateLimitMiddleware(RateLimitConfigs.AUTH_AUTHORIZE_IP), Validator('json', AuthorizeIpRequest), OpenAPI({ operationId: 'authorize_ip_address', summary: 'Authorize IP address', responseSchema: null, statusCode: 204, security: [], tags: ['Auth'], description: 'Verify and authorize a new IP address using the confirmation code sent via email. Completes IP authorization flow.', }), async (ctx) => { await ctx.get('authRequestService').completeIpAuthorization({data: ctx.req.valid('json')}); return ctx.body(null, 204); }, ); app.post( '/auth/ip-authorization/resend', LocalAuthMiddleware, RateLimitMiddleware(RateLimitConfigs.AUTH_IP_AUTHORIZATION_RESEND), Validator('json', MfaTicketRequest), OpenAPI({ operationId: 'resend_ip_authorization', summary: 'Resend IP authorization', responseSchema: null, statusCode: 204, security: [], tags: ['Auth'], description: 'Request a new IP authorization verification code to be sent via email. Use if the original code was lost or expired.', }), async (ctx) => { await ctx.get('authRequestService').resendIpAuthorization(ctx.req.valid('json')); return ctx.body(null, 204); }, ); app.get( '/auth/ip-authorization/poll', LocalAuthMiddleware, RateLimitMiddleware(RateLimitConfigs.AUTH_IP_AUTHORIZATION_POLL), Validator('query', IpAuthorizationPollQuery), OpenAPI({ operationId: 'poll_ip_authorization', summary: 'Poll IP authorization', responseSchema: IpAuthorizationPollResponse, statusCode: 200, security: [], tags: ['Auth'], description: 'Poll the status of an IP authorization request. Use the ticket parameter to check if verification has been completed.', }), async (ctx) => { const {ticket} = ctx.req.valid('query'); return ctx.json(await ctx.get('authRequestService').pollIpAuthorization({ticket})); }, ); app.post( '/auth/webauthn/authentication-options', LocalAuthMiddleware, RateLimitMiddleware(RateLimitConfigs.AUTH_WEBAUTHN_OPTIONS), OpenAPI({ operationId: 'get_webauthn_authentication_options', summary: 'Get WebAuthn authentication options', responseSchema: WebAuthnAuthenticationOptionsResponse, statusCode: 200, security: [], tags: ['Auth'], description: 'Retrieve WebAuthn authentication challenge and options for passwordless login with biometrics or security keys.', }), async (ctx) => { return ctx.json(await ctx.get('authRequestService').getWebAuthnAuthenticationOptions()); }, ); app.post( '/auth/webauthn/authenticate', LocalAuthMiddleware, RateLimitMiddleware(RateLimitConfigs.AUTH_WEBAUTHN_AUTHENTICATE), Validator('json', WebAuthnAuthenticateRequest), OpenAPI({ operationId: 'authenticate_with_webauthn', summary: 'Authenticate with WebAuthn', responseSchema: AuthTokenWithUserIdResponse, statusCode: 200, security: [], tags: ['Auth'], description: 'Complete passwordless login using WebAuthn (biometrics or security key). Returns authentication token on success.', }), async (ctx) => { return ctx.json( await ctx.get('authRequestService').authenticateWebAuthnDiscoverable({ data: ctx.req.valid('json'), request: ctx.req.raw, }), ); }, ); app.post( '/auth/login/mfa/webauthn/authentication-options', LocalAuthMiddleware, RateLimitMiddleware(RateLimitConfigs.AUTH_LOGIN_MFA), Validator('json', MfaTicketRequest), OpenAPI({ operationId: 'get_webauthn_mfa_options', summary: 'Get WebAuthn MFA options', responseSchema: WebAuthnAuthenticationOptionsResponse, statusCode: 200, security: [], tags: ['Auth'], description: 'Retrieve WebAuthn challenge and options for multi-factor authentication. Requires the MFA ticket from initial login.', }), async (ctx) => { return ctx.json(await ctx.get('authRequestService').getWebAuthnMfaOptions(ctx.req.valid('json'))); }, ); app.post( '/auth/login/mfa/webauthn', LocalAuthMiddleware, RateLimitMiddleware(RateLimitConfigs.AUTH_LOGIN_MFA), Validator('json', WebAuthnMfaRequest), OpenAPI({ operationId: 'login_with_webauthn_mfa', summary: 'Login with WebAuthn MFA', responseSchema: AuthTokenWithUserIdResponse, statusCode: 200, security: [], tags: ['Auth'], description: 'Complete login by verifying WebAuthn response during MFA. Requires the MFA ticket from initial login attempt.', }), async (ctx) => { const result = await ctx.get('authRequestService').loginMfaWebAuthn({ data: ctx.req.valid('json'), request: ctx.req.raw, }); return ctx.json(result); }, ); app.post( '/auth/username-suggestions', LocalAuthMiddleware, RateLimitMiddleware(RateLimitConfigs.AUTH_REGISTER), Validator('json', UsernameSuggestionsRequest), OpenAPI({ operationId: 'get_username_suggestions', summary: 'Get username suggestions', responseSchema: UsernameSuggestionsResponse, statusCode: 200, security: [], tags: ['Auth'], description: 'Generate username suggestions based on a provided global name for new account registration.', }), async (ctx) => { const response = ctx.get('authRequestService').getUsernameSuggestions({ globalName: ctx.req.valid('json').global_name, }); return ctx.json(response); }, ); app.post( '/auth/handoff/initiate', RateLimitMiddleware(RateLimitConfigs.AUTH_HANDOFF_INITIATE), OpenAPI({ operationId: 'initiate_handoff', summary: 'Initiate handoff', responseSchema: HandoffInitiateResponse, statusCode: 200, security: [], tags: ['Auth'], description: 'Start a handoff session to transfer authentication between devices. Returns a handoff code for device linking.', }), async (ctx) => { const clientIp = requireClientIp(ctx.req.raw, { trustClientIpHeader: Config.proxy.trust_client_ip_header, clientIpHeaderName: Config.proxy.client_ip_header, }); const clientPlatform = ctx.req.header('x-fluxer-platform')?.trim().toLowerCase() ?? undefined; return ctx.json( await ctx.get('authRequestService').initiateHandoff({ userAgent: ctx.req.header('User-Agent'), clientIp, clientPlatform, }), ); }, ); app.get( '/auth/handoff/:code/info', RateLimitMiddleware(RateLimitConfigs.AUTH_HANDOFF_INFO), Validator('param', HandoffCodeParam), OpenAPI({ operationId: 'get_handoff_info', summary: 'Get handoff info', responseSchema: HandoffInfoResponse, statusCode: 200, security: [], tags: ['Auth'], description: 'Retrieve device and location information about a pending handoff request. Non-destructive – the code remains valid after this call.', }), async (ctx) => { const clientIp = requireClientIp(ctx.req.raw, { trustClientIpHeader: Config.proxy.trust_client_ip_header, clientIpHeaderName: Config.proxy.client_ip_header, }); const response = await ctx.get('authRequestService').getHandoffInfo({ code: ctx.req.valid('param').code, clientIp, }); return ctx.json(response); }, ); app.post( '/auth/handoff/complete', RateLimitMiddleware(RateLimitConfigs.AUTH_HANDOFF_COMPLETE), Validator('json', HandoffCompleteRequest), OpenAPI({ operationId: 'complete_handoff', summary: 'Complete handoff', responseSchema: null, statusCode: 204, security: [], tags: ['Auth'], description: 'Complete the handoff process and authenticate on the target device using the handoff code.', }), async (ctx) => { const clientIp = requireClientIp(ctx.req.raw, { trustClientIpHeader: Config.proxy.trust_client_ip_header, clientIpHeaderName: Config.proxy.client_ip_header, }); await ctx.get('authRequestService').completeHandoff({ data: ctx.req.valid('json'), request: ctx.req.raw, clientIp, authToken: ctx.get('authToken') ?? undefined, }); return ctx.body(null, 204); }, ); app.get( '/auth/handoff/:code/status', RateLimitMiddleware(RateLimitConfigs.AUTH_HANDOFF_STATUS), Validator('param', HandoffCodeParam), OpenAPI({ operationId: 'get_handoff_status', summary: 'Get handoff status', responseSchema: HandoffStatusResponse, statusCode: 200, security: [], tags: ['Auth'], description: 'Check the status of a handoff session. Returns whether the handoff has been completed or is still pending.', }), async (ctx) => { const clientIp = requireClientIp(ctx.req.raw, { trustClientIpHeader: Config.proxy.trust_client_ip_header, clientIpHeaderName: Config.proxy.client_ip_header, }); const response = await ctx.get('authRequestService').getHandoffStatus({ code: ctx.req.valid('param').code, clientIp, }); return ctx.json(response); }, ); app.delete( '/auth/handoff/:code', RateLimitMiddleware(RateLimitConfigs.AUTH_HANDOFF_CANCEL), Validator('param', HandoffCodeParam), OpenAPI({ operationId: 'cancel_handoff', summary: 'Cancel handoff', responseSchema: null, statusCode: 204, security: [], tags: ['Auth'], description: 'Cancel an ongoing handoff session. The handoff code will no longer be valid for authentication.', }), async (ctx) => { await ctx.get('authRequestService').cancelHandoff({code: ctx.req.valid('param').code}); return ctx.body(null, 204); }, );}