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

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>