Implement OIDC login
This commit is contained in:
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
@ -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.
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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>
|
||||
@ -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>
|
||||
|
||||
@ -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({
|
||||
|
||||
@ -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')
|
||||
|
||||
@ -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>
|
||||
|
||||
Reference in New Issue
Block a user