Implement OIDC login

This commit is contained in:
2025-07-16 21:44:20 +02:00
parent f0c729cba7
commit 33c1e7278b
11 changed files with 385 additions and 37 deletions

View File

@ -1,14 +1,26 @@
"""Authentication related endpoints factory."""
# pyright: reportUnusedFunction=false
import os
from datetime import datetime
import logging
from typing import Annotated, Literal
from fastapi import APIRouter, Depends, Form, HTTPException, Request, Security, status
from fastapi import (
APIRouter,
Depends,
Form,
HTTPException,
Request,
Security,
status,
)
from fastapi.responses import RedirectResponse
from fastapi.security import OAuth2PasswordRequestForm
from pydantic import BaseModel
from pydantic import BaseModel, ValidationError
from sqlalchemy.ext.asyncio import AsyncSession
from sshecret_admin.auth import (
LocalUserInfo,
Token,
User,
authenticate_user_async,
@ -16,8 +28,12 @@ from sshecret_admin.auth import (
create_refresh_token,
decode_token,
)
from sshecret_admin.auth.authentication import hash_password
from sshecret_admin.auth.authentication import handle_oidc_claim, hash_password
from sshecret_admin.auth.exceptions import AuthenticationFailedError
from sshecret_admin.auth.models import AuthProvider, LoginInfo
from sshecret_admin.auth.oidc import AdminOidc
from sshecret_admin.core.dependencies import AdminDependencies
from sshecret_admin.core.settings import AdminServerSettings
from sshecret_admin.services import AdminBackend
from sshecret_admin.services.models import UserPasswordChange
@ -37,6 +53,16 @@ def create_router(dependencies: AdminDependencies) -> APIRouter:
"""Create auth router."""
app = APIRouter()
def get_oidc_client() -> AdminOidc:
"""Get OIDC client dependency."""
if not dependencies.settings.oidc:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="OIDC authentication not available.",
)
oidc = AdminOidc(dependencies.settings.oidc)
return oidc
@app.post("/token")
async def login_for_access_token(
session: Annotated[AsyncSession, Depends(dependencies.get_async_session)],
@ -122,4 +148,79 @@ def create_router(dependencies: AdminDependencies) -> APIRouter:
username=user.username,
)
@app.get("/oidc/login")
async def start_oidc_login(
request: Request, oidc: Annotated[AdminOidc, Depends(get_oidc_client)]
) -> RedirectResponse:
"""Redirect for OIDC login."""
redirect_url = request.url_for("oidc_callback")
return await oidc.start_auth(request, redirect_url)
@app.get("/oidc/callback")
async def oidc_callback(
request: Request,
session: Annotated[AsyncSession, Depends(dependencies.get_async_session)],
admin: Annotated[AdminBackend, Depends(dependencies.get_admin_backend)],
oidc: Annotated[AdminOidc, Depends(get_oidc_client)],
):
"""Callback for OIDC auth."""
try:
claims = await oidc.handle_auth_callback(request)
except AuthenticationFailedError as error:
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail=error)
except ValidationError as error:
LOG.error("Validation error: %s", error, exc_info=True)
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail=error)
# We now have a IdentityClaims object.
# We need to check if this matches an existing user, or we need to create a new one.
user = await handle_oidc_claim(session, claims)
user.last_login = datetime.now()
session.add(user)
await session.commit()
# Set cookies
token_data: dict[str, str] = {"sub": claims.sub}
access_token = create_access_token(
dependencies.settings, data=token_data, provider=claims.provider
)
refresh_token = create_refresh_token(
dependencies.settings, data=token_data, provider=claims.provider
)
path = f"/auth_cb#access_token={access_token}&refresh_token={refresh_token}"
callback_url = os.path.join(dependencies.settings.frontend_url, path)
origin = "UNKNOWN"
if request.client:
origin = request.client.host
await admin.write_audit_message(
operation=Operation.LOGIN,
message="Logged in to admin frontend",
origin=origin,
username=user.username,
oidc=claims.provider,
)
return RedirectResponse(callback_url)
@app.get("/users/me")
async def get_current_user(
current_user: Annotated[User, Security(dependencies.get_current_active_user)],
) -> LocalUserInfo:
"""Get information about the user currently logged in."""
is_local = current_user.provider is AuthProvider.LOCAL
return LocalUserInfo(
id=current_user.id, display_name=current_user.username, local=is_local
)
@app.get("/oidc/status")
async def get_auth_info() -> LoginInfo:
"""Check if OIDC login is available."""
if dependencies.settings.oidc:
return LoginInfo(
enabled=True, oidc_provider=dependencies.settings.oidc.name
)
return LoginInfo(enabled=False)
return app

View File

@ -189,3 +189,8 @@ class LocalUserInfo(BaseModel):
local: bool
class LoginInfo(BaseModel):
"""Model containing information about login providers."""
enabled: bool
oidc_provider: str | None = None

View File

@ -40,6 +40,7 @@ class AdminServerSettings(BaseSettings):
password_manager_directory: Path | None = None
oidc: OidcSettings | None = None
frontend_origin: str = Field(default="*")
frontend_url: str
@property
def admin_db(self) -> URL:

File diff suppressed because one or more lines are too long

View File

@ -1,7 +1,7 @@
// This file is auto-generated by @hey-api/openapi-ts
import { type Options as ClientOptions, type TDataShape, type Client, urlSearchParamsBodySerializer } from './client';
import type { GetHealthHealthGetData, GetHealthHealthGetResponses, GetAuditLogApiV1AuditGetData, GetAuditLogApiV1AuditGetResponses, GetAuditLogApiV1AuditGetErrors, LoginForAccessTokenApiV1TokenPostData, LoginForAccessTokenApiV1TokenPostResponses, LoginForAccessTokenApiV1TokenPostErrors, RefreshTokenApiV1RefreshPostData, RefreshTokenApiV1RefreshPostResponses, RefreshTokenApiV1RefreshPostErrors, ChangePasswordApiV1PasswordPostData, ChangePasswordApiV1PasswordPostResponses, ChangePasswordApiV1PasswordPostErrors, GetClientsApiV1ClientsGetData, GetClientsApiV1ClientsGetResponses, CreateClientApiV1ClientsPostData, CreateClientApiV1ClientsPostResponses, CreateClientApiV1ClientsPostErrors, GetClientsTerseApiV1ClientsTerseGetData, GetClientsTerseApiV1ClientsTerseGetResponses, QueryClientsApiV1QueryClientsGetData, QueryClientsApiV1QueryClientsGetResponses, QueryClientsApiV1QueryClientsGetErrors, DeleteClientApiV1ClientsIdDeleteData, DeleteClientApiV1ClientsIdDeleteResponses, DeleteClientApiV1ClientsIdDeleteErrors, GetClientApiV1ClientsIdGetData, GetClientApiV1ClientsIdGetResponses, GetClientApiV1ClientsIdGetErrors, UpdateClientApiV1ClientsIdPutData, UpdateClientApiV1ClientsIdPutResponses, UpdateClientApiV1ClientsIdPutErrors, DeleteSecretFromClientApiV1ClientsIdSecretsSecretNameDeleteData, DeleteSecretFromClientApiV1ClientsIdSecretsSecretNameDeleteResponses, DeleteSecretFromClientApiV1ClientsIdSecretsSecretNameDeleteErrors, AddSecretToClientApiV1ClientsIdSecretsSecretNamePutData, AddSecretToClientApiV1ClientsIdSecretsSecretNamePutResponses, AddSecretToClientApiV1ClientsIdSecretsSecretNamePutErrors, UpdateClientPoliciesApiV1ClientsIdPoliciesPutData, UpdateClientPoliciesApiV1ClientsIdPoliciesPutResponses, UpdateClientPoliciesApiV1ClientsIdPoliciesPutErrors, UpdateClientPublicKeyApiV1ClientsIdPublicKeyPutData, UpdateClientPublicKeyApiV1ClientsIdPublicKeyPutResponses, UpdateClientPublicKeyApiV1ClientsIdPublicKeyPutErrors, GetSystemStatsApiV1StatsGetData, GetSystemStatsApiV1StatsGetResponses, GetSecretNamesApiV1SecretsGetData, GetSecretNamesApiV1SecretsGetResponses, AddSecretApiV1SecretsPostData, AddSecretApiV1SecretsPostResponses, AddSecretApiV1SecretsPostErrors, DeleteSecretApiV1SecretsNameDeleteData, DeleteSecretApiV1SecretsNameDeleteResponses, DeleteSecretApiV1SecretsNameDeleteErrors, GetSecretApiV1SecretsNameGetData, GetSecretApiV1SecretsNameGetResponses, GetSecretApiV1SecretsNameGetErrors, UpdateSecretApiV1SecretsNamePutData, UpdateSecretApiV1SecretsNamePutResponses, UpdateSecretApiV1SecretsNamePutErrors, GetSecretGroupsApiV1SecretsGroupsGetData, GetSecretGroupsApiV1SecretsGroupsGetResponses, GetSecretGroupsApiV1SecretsGroupsGetErrors, AddSecretGroupApiV1SecretsGroupsPostData, AddSecretGroupApiV1SecretsGroupsPostResponses, AddSecretGroupApiV1SecretsGroupsPostErrors, DeleteSecretGroupApiV1SecretsGroupsGroupPathDeleteData, DeleteSecretGroupApiV1SecretsGroupsGroupPathDeleteResponses, DeleteSecretGroupApiV1SecretsGroupsGroupPathDeleteErrors, GetSecretGroupApiV1SecretsGroupsGroupPathGetData, GetSecretGroupApiV1SecretsGroupsGroupPathGetResponses, GetSecretGroupApiV1SecretsGroupsGroupPathGetErrors, UpdateSecretGroupApiV1SecretsGroupsGroupPathPutData, UpdateSecretGroupApiV1SecretsGroupsGroupPathPutResponses, UpdateSecretGroupApiV1SecretsGroupsGroupPathPutErrors, DeleteGroupIdApiV1SecretsGroupIdDeleteData, DeleteGroupIdApiV1SecretsGroupIdDeleteResponses, DeleteGroupIdApiV1SecretsGroupIdDeleteErrors, AssignSecretGroupApiV1SecretsSetGroupPostData, AssignSecretGroupApiV1SecretsSetGroupPostResponses, AssignSecretGroupApiV1SecretsSetGroupPostErrors, MoveGroupApiV1SecretsMoveGroupGroupNamePostData, MoveGroupApiV1SecretsMoveGroupGroupNamePostResponses, MoveGroupApiV1SecretsMoveGroupGroupNamePostErrors } from './types.gen';
import type { GetHealthHealthGetData, GetHealthHealthGetResponses, GetAuditLogApiV1AuditGetData, GetAuditLogApiV1AuditGetResponses, GetAuditLogApiV1AuditGetErrors, LoginForAccessTokenApiV1TokenPostData, LoginForAccessTokenApiV1TokenPostResponses, LoginForAccessTokenApiV1TokenPostErrors, RefreshTokenApiV1RefreshPostData, RefreshTokenApiV1RefreshPostResponses, RefreshTokenApiV1RefreshPostErrors, ChangePasswordApiV1PasswordPostData, ChangePasswordApiV1PasswordPostResponses, ChangePasswordApiV1PasswordPostErrors, StartOidcLoginApiV1OidcLoginGetData, StartOidcLoginApiV1OidcLoginGetResponses, OidcCallbackApiV1OidcCallbackGetData, OidcCallbackApiV1OidcCallbackGetResponses, GetCurrentUserApiV1UsersMeGetData, GetCurrentUserApiV1UsersMeGetResponses, GetAuthInfoApiV1OidcStatusGetData, GetAuthInfoApiV1OidcStatusGetResponses, GetClientsApiV1ClientsGetData, GetClientsApiV1ClientsGetResponses, CreateClientApiV1ClientsPostData, CreateClientApiV1ClientsPostResponses, CreateClientApiV1ClientsPostErrors, GetClientsTerseApiV1ClientsTerseGetData, GetClientsTerseApiV1ClientsTerseGetResponses, QueryClientsApiV1QueryClientsGetData, QueryClientsApiV1QueryClientsGetResponses, QueryClientsApiV1QueryClientsGetErrors, DeleteClientApiV1ClientsIdDeleteData, DeleteClientApiV1ClientsIdDeleteResponses, DeleteClientApiV1ClientsIdDeleteErrors, GetClientApiV1ClientsIdGetData, GetClientApiV1ClientsIdGetResponses, GetClientApiV1ClientsIdGetErrors, UpdateClientApiV1ClientsIdPutData, UpdateClientApiV1ClientsIdPutResponses, UpdateClientApiV1ClientsIdPutErrors, DeleteSecretFromClientApiV1ClientsIdSecretsSecretNameDeleteData, DeleteSecretFromClientApiV1ClientsIdSecretsSecretNameDeleteResponses, DeleteSecretFromClientApiV1ClientsIdSecretsSecretNameDeleteErrors, AddSecretToClientApiV1ClientsIdSecretsSecretNamePutData, AddSecretToClientApiV1ClientsIdSecretsSecretNamePutResponses, AddSecretToClientApiV1ClientsIdSecretsSecretNamePutErrors, UpdateClientPoliciesApiV1ClientsIdPoliciesPutData, UpdateClientPoliciesApiV1ClientsIdPoliciesPutResponses, UpdateClientPoliciesApiV1ClientsIdPoliciesPutErrors, UpdateClientPublicKeyApiV1ClientsIdPublicKeyPutData, UpdateClientPublicKeyApiV1ClientsIdPublicKeyPutResponses, UpdateClientPublicKeyApiV1ClientsIdPublicKeyPutErrors, GetSystemStatsApiV1StatsGetData, GetSystemStatsApiV1StatsGetResponses, GetSecretNamesApiV1SecretsGetData, GetSecretNamesApiV1SecretsGetResponses, AddSecretApiV1SecretsPostData, AddSecretApiV1SecretsPostResponses, AddSecretApiV1SecretsPostErrors, DeleteSecretApiV1SecretsNameDeleteData, DeleteSecretApiV1SecretsNameDeleteResponses, DeleteSecretApiV1SecretsNameDeleteErrors, GetSecretApiV1SecretsNameGetData, GetSecretApiV1SecretsNameGetResponses, GetSecretApiV1SecretsNameGetErrors, UpdateSecretApiV1SecretsNamePutData, UpdateSecretApiV1SecretsNamePutResponses, UpdateSecretApiV1SecretsNamePutErrors, GetSecretGroupsApiV1SecretsGroupsGetData, GetSecretGroupsApiV1SecretsGroupsGetResponses, GetSecretGroupsApiV1SecretsGroupsGetErrors, AddSecretGroupApiV1SecretsGroupsPostData, AddSecretGroupApiV1SecretsGroupsPostResponses, AddSecretGroupApiV1SecretsGroupsPostErrors, DeleteSecretGroupApiV1SecretsGroupsGroupPathDeleteData, DeleteSecretGroupApiV1SecretsGroupsGroupPathDeleteResponses, DeleteSecretGroupApiV1SecretsGroupsGroupPathDeleteErrors, GetSecretGroupApiV1SecretsGroupsGroupPathGetData, GetSecretGroupApiV1SecretsGroupsGroupPathGetResponses, GetSecretGroupApiV1SecretsGroupsGroupPathGetErrors, UpdateSecretGroupApiV1SecretsGroupsGroupPathPutData, UpdateSecretGroupApiV1SecretsGroupsGroupPathPutResponses, UpdateSecretGroupApiV1SecretsGroupsGroupPathPutErrors, DeleteGroupIdApiV1SecretsGroupIdDeleteData, DeleteGroupIdApiV1SecretsGroupIdDeleteResponses, DeleteGroupIdApiV1SecretsGroupIdDeleteErrors, AssignSecretGroupApiV1SecretsSetGroupPostData, AssignSecretGroupApiV1SecretsSetGroupPostResponses, AssignSecretGroupApiV1SecretsSetGroupPostErrors, MoveGroupApiV1SecretsMoveGroupGroupNamePostData, MoveGroupApiV1SecretsMoveGroupGroupNamePostResponses, MoveGroupApiV1SecretsMoveGroupGroupNamePostErrors } from './types.gen';
import { client as _heyApiClient } from './client.gen';
export type Options<TData extends TDataShape = TDataShape, ThrowOnError extends boolean = boolean> = ClientOptions<TData, ThrowOnError> & {
@ -105,6 +105,60 @@ export class SshecretAdmin {
});
}
/**
* Start Oidc Login
* Redirect for OIDC login.
*/
public static startOidcLoginApiV1OidcLoginGet<ThrowOnError extends boolean = false>(options?: Options<StartOidcLoginApiV1OidcLoginGetData, ThrowOnError>) {
return (options?.client ?? _heyApiClient).get<StartOidcLoginApiV1OidcLoginGetResponses, unknown, ThrowOnError>({
responseType: 'json',
url: '/api/v1/oidc/login',
...options
});
}
/**
* Oidc Callback
* Callback for OIDC auth.
*/
public static oidcCallbackApiV1OidcCallbackGet<ThrowOnError extends boolean = false>(options?: Options<OidcCallbackApiV1OidcCallbackGetData, ThrowOnError>) {
return (options?.client ?? _heyApiClient).get<OidcCallbackApiV1OidcCallbackGetResponses, unknown, ThrowOnError>({
responseType: 'json',
url: '/api/v1/oidc/callback',
...options
});
}
/**
* Get Current User
* Get information about the user currently logged in.
*/
public static getCurrentUserApiV1UsersMeGet<ThrowOnError extends boolean = false>(options?: Options<GetCurrentUserApiV1UsersMeGetData, ThrowOnError>) {
return (options?.client ?? _heyApiClient).get<GetCurrentUserApiV1UsersMeGetResponses, unknown, ThrowOnError>({
responseType: 'json',
security: [
{
scheme: 'bearer',
type: 'http'
}
],
url: '/api/v1/users/me',
...options
});
}
/**
* Get Auth Info
* Check if OIDC login is available.
*/
public static getAuthInfoApiV1OidcStatusGet<ThrowOnError extends boolean = false>(options?: Options<GetAuthInfoApiV1OidcStatusGetData, ThrowOnError>) {
return (options?.client ?? _heyApiClient).get<GetAuthInfoApiV1OidcStatusGetResponses, unknown, ThrowOnError>({
responseType: 'json',
url: '/api/v1/oidc/status',
...options
});
}
/**
* Get Clients
* Get clients.

View File

@ -309,6 +309,40 @@ export type HttpValidationError = {
detail?: Array<ValidationError>;
};
/**
* LocalUserInfo
* Model used to present a user in the web ui.
*/
export type LocalUserInfo = {
/**
* Id
*/
id: string;
/**
* Display Name
*/
display_name: string;
/**
* Local
*/
local: boolean;
};
/**
* LoginInfo
* Model containing information about login providers.
*/
export type LoginInfo = {
/**
* Enabled
*/
enabled: boolean;
/**
* Oidc Provider
*/
oidc_provider?: string | null;
};
/**
* Operation
* Various operations for the audit logging module.
@ -754,6 +788,66 @@ export type ChangePasswordApiV1PasswordPostResponses = {
200: unknown;
};
export type StartOidcLoginApiV1OidcLoginGetData = {
body?: never;
path?: never;
query?: never;
url: '/api/v1/oidc/login';
};
export type StartOidcLoginApiV1OidcLoginGetResponses = {
/**
* Successful Response
*/
200: unknown;
};
export type OidcCallbackApiV1OidcCallbackGetData = {
body?: never;
path?: never;
query?: never;
url: '/api/v1/oidc/callback';
};
export type OidcCallbackApiV1OidcCallbackGetResponses = {
/**
* Successful Response
*/
200: unknown;
};
export type GetCurrentUserApiV1UsersMeGetData = {
body?: never;
path?: never;
query?: never;
url: '/api/v1/users/me';
};
export type GetCurrentUserApiV1UsersMeGetResponses = {
/**
* Successful Response
*/
200: LocalUserInfo;
};
export type GetCurrentUserApiV1UsersMeGetResponse = GetCurrentUserApiV1UsersMeGetResponses[keyof GetCurrentUserApiV1UsersMeGetResponses];
export type GetAuthInfoApiV1OidcStatusGetData = {
body?: never;
path?: never;
query?: never;
url: '/api/v1/oidc/status';
};
export type GetAuthInfoApiV1OidcStatusGetResponses = {
/**
* Successful Response
*/
200: LoginInfo;
};
export type GetAuthInfoApiV1OidcStatusGetResponse = GetAuthInfoApiV1OidcStatusGetResponses[keyof GetAuthInfoApiV1OidcStatusGetResponses];
export type GetClientsApiV1ClientsGetData = {
body?: never;
path?: never;

View File

@ -0,0 +1,21 @@
<script setup lang="ts">
import { onMounted } from 'vue'
import { useRouter, useRoute } from 'vue-router'
import { useAuthTokenStore } from '@/store/auth'
const router = useRouter()
const route = useRoute()
const auth = useAuthTokenStore()
function getTokens() {
const hash = window.location.hash.substring(1)
const params = new URLSearchParams(hash)
const accessToken = params.get('access_token')
const refreshToken = params.get('refresh_token')
auth.setToken(accessToken, refreshToken)
router.push({ name: 'dashboard' })
}
onMounted(getTokens)
</script>

View File

@ -18,7 +18,7 @@
<sl-menu>
<sl-menu-item>{{ auth.username }}</sl-menu-item>
<sl-divider></sl-divider>
<sl-menu-item @click="showDrawer">Change Password</sl-menu-item>
<sl-menu-item @click="showDrawer" v-if="!auth.oidcUser">Change Password</sl-menu-item>
<sl-menu-item @click="logout"> Logout </sl-menu-item>
</sl-menu>
</sl-dropdown>
@ -31,7 +31,7 @@
</Drawer>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { onMounted, ref } from 'vue'
import { useRouter } from 'vue-router'
import { useAuthTokenStore } from '@/store/auth'
@ -44,6 +44,8 @@ const router = useRouter()
const showPasswordDrawer = ref<boolean>(false)
const localUser = ref(true)
function logout() {
auth.logout()
router.push('/login')
@ -52,6 +54,12 @@ function logout() {
function showDrawer() {
showPasswordDrawer.value = true
}
async function getUsername() {
await auth.getUserInfo()
}
onMounted(getUsername)
</script>
<style scoped>

View File

@ -9,6 +9,7 @@ import ClientDetailPage from '@/views/clients/ClientDetailPage.vue'
import SecretDetailView from '@/views/secrets/SecretDetailView.vue'
import SecretGroupDetailView from '@/views/secrets/SecretGroupDetailView.vue'
import GenericDetail from '@/components/common/GenericDetail.vue'
import OidcCallback from '@/components/auth/OidcCallback.vue'
import { useAuthTokenStore } from '@/store/auth'
import { reassemblePath } from '@/api/paths'
@ -73,7 +74,12 @@ const routes = [
meta: { requiresAuth: true },
}
],
}
},
{
path: '/auth_cb',
name: 'oidcCallback',
component: OidcCallback,
},
]
const router = createRouter({

View File

@ -3,6 +3,7 @@ import { defineStore } from 'pinia'
import { client } from '@/client/client.gen'
import { SshecretAdmin } from '@/client'
import type { Token } from '@/client'
import { assertSdkResponseOk } from '@/api/assertSdkResponseOk'
export function setAuthToken(token: string | null) {
client.setConfig({
@ -16,6 +17,7 @@ export const useAuthTokenStore = defineStore('authtoken', {
refreshToken: '' as string,
isLoggedIn: false,
username: '' as string,
oidcUser: false as boolean,
}),
actions: {
async login(username: string, password: string): Promise<boolean> {
@ -38,6 +40,15 @@ export const useAuthTokenStore = defineStore('authtoken', {
return false
}
},
setToken(accessToken: string, refreshToken: string) {
this.accessToken = accessToken
this.refreshToken = refreshToken
this.isLoggedIn = true
localStorage.setItem('accessToken', accessToken)
localStorage.setItem('refreshToken', refreshToken)
setAuthToken(this.accessToken)
},
async refresh(): Promise<boolean> {
try {
console.log("Refreshing token")
@ -57,6 +68,31 @@ export const useAuthTokenStore = defineStore('authtoken', {
return false
}
},
async getUserInfo() {
try {
const response = await SshecretAdmin.getCurrentUserApiV1UsersMeGet()
const responseData = assertSdkResponseOk(response)
this.username = responseData.display_name
this.oidcUser = !responseData.local
} catch (err) {
console.log(err)
this.logout()
}
},
async getOidcProvider(): Promise<string | null> {
try {
const response = await SshecretAdmin.getAuthInfoApiV1OidcStatusGet()
const responseData = assertSdkResponseOk(response)
console.log(responseData)
if (responseData.enabled && responseData.oidc_provider) {
console.log('Yes')
return responseData.oidc_provider
}
} catch (err) {
console.log(err)
}
return null
},
loadFromStorage() {
// Load token from user storage.
const accessToken = localStorage.getItem('accessToken')

View File

@ -1,70 +1,92 @@
<template>
<div class="min-h-screen bg-gray-100 flex items-center justify-center p-4">
<div v-if="error" class="w-screen absolute top-0 left-0 z-50">
<sl-alert variant="danger" open v-if="error">
<sl-icon slot="icon" name="exclamation-octagon"></sl-icon>
<strong>Login failed.</strong><br />
Please check your username and password, and try again.
</sl-alert>
</div>
<div class="max-w-md w-full bg-white rounded-xl shadow-lg p-8">
<h2 class="text-2xl font-bold text-gray-900 mb-6 text-center">Sign In</h2>
<form @submit.prevent="submitLogin" class="space-y-4">
<div>
<label class="block text-sm font-medium text-gray-400 mb-1">Username</label>
<input
v-model="username"
type="text"
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 outline-none transition-all"
<sl-input
label="Username"
class="w-full"
placeholder="Username"
autocomplete="username"
required=""
/>
:value="username"
@input="username = $event.target.value"
></sl-input>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Password</label>
<input
v-model="password"
<sl-input
label="Password"
type="password"
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 outline-none transition-all"
class="w-full"
:value="password"
@input="password = $event.target.value"
placeholder="••••••••"
autocomplete="current-password"
/>
required=""
></sl-input>
</div>
<button
type="submit"
class="w-full bg-indigo-600 hover:bg-indigo-700 text-white font-medium py-2.5 rounded-lg transition-colors"
>
Login
</button>
<sl-button type="submit" variant="primary" class="w-full"> Login </sl-button>
</form>
<template v-if="oidcProvider">
<div class="w-full items-center text-center my-4 flex">
<div class="w-full h-[0.125rem] box-border bg-gray-200 dark:bg-gray-700"></div>
<div class="px-4 text-lg text-sm font-medium text-gray-500 dark:text-gray-400">Or</div>
<div class="w-full h-[0.125rem] box-border bg-gray-200 dark:bg-gray-700"></div>
</div>
<div class="w-full text-center my-4">
<sl-button outline variant="neutral" :href="oidcLoginUrl">
Sign in with {{ oidcProvider }}
</sl-button>
</div>
</template>
</div>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { onMounted, ref } from 'vue'
import { useAuthTokenStore } from '@/store/auth'
import { useRouter } from 'vue-router'
import { useAlertsStore } from '@/store/useAlertsStore'
import '@shoelace-style/shoelace/dist/components/alert/alert.js'
const username = ref('')
const password = ref('')
const error = ref(false)
const auth = useAuthTokenStore()
const router = useRouter()
const alerts = useAlertsStore()
const oidcProvider = ref<string | null>()
async function checkOidcProvider() {
const provider = await auth.getOidcProvider()
console.log(provider)
if (provider) {
console.log('OIDC Provider: ', provider)
oidcProvider.value = provider
}
}
const baseURL = import.meta.env.SSHECRET_FRONTEND_API_BASE_URL
const oidcLoginUrl = baseURL + '/api/v1/oidc/login'
async function submitLogin() {
error.value = false
const success = await auth.login(username.value, password.value)
if (success) {
router.push('/')
} else {
error.value = true
alerts.showAlert(
'Please check your username and password, and try again.',
'error',
'Login Failed',
)
}
}
onMounted(checkOidcProvider)
</script>