✨ Add OIDC authentication
This commit is contained in:
parent
8af664b9e7
commit
03b294f138
@ -1 +1 @@
|
|||||||
__version__ = "1.5.0"
|
__version__ = "1.6.0"
|
||||||
|
|||||||
@ -17,6 +17,14 @@ class Settings(BaseSettings):
|
|||||||
ACCESS_TOKEN_EXPIRE_MINUTES: int = 30
|
ACCESS_TOKEN_EXPIRE_MINUTES: int = 30
|
||||||
REFRESH_TOKEN_EXPIRE_MINUTES: int = 1440
|
REFRESH_TOKEN_EXPIRE_MINUTES: int = 1440
|
||||||
|
|
||||||
|
REGISTER_ENABLE: bool = True
|
||||||
|
OIDC_PROTOCOL: str = "https"
|
||||||
|
OIDC_CLIENT_ID: str = ""
|
||||||
|
OIDC_CLIENT_SECRET: str = ""
|
||||||
|
OIDC_HOST: str = ""
|
||||||
|
OIDC_REALM: str = "master"
|
||||||
|
OIDC_REDIRECT_URI: str = ""
|
||||||
|
|
||||||
class Config:
|
class Config:
|
||||||
env_file = "storage/config.yml"
|
env_file = "storage/config.yml"
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
from typing import Annotated
|
from typing import Annotated
|
||||||
|
|
||||||
import jwt
|
import jwt
|
||||||
|
from authlib.integrations.httpx_client import OAuth2Client
|
||||||
from fastapi import Depends, HTTPException
|
from fastapi import Depends, HTTPException
|
||||||
from fastapi.security import OAuth2PasswordBearer
|
from fastapi.security import OAuth2PasswordBearer
|
||||||
from sqlmodel import Session
|
from sqlmodel import Session
|
||||||
@ -9,7 +10,7 @@ from .config import settings
|
|||||||
from .db.core import get_engine
|
from .db.core import get_engine
|
||||||
from .models.models import User
|
from .models.models import User
|
||||||
|
|
||||||
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/auth/login")
|
oauth_password_scheme = OAuth2PasswordBearer(tokenUrl="/auth/login")
|
||||||
|
|
||||||
|
|
||||||
def get_session():
|
def get_session():
|
||||||
@ -21,7 +22,7 @@ def get_session():
|
|||||||
SessionDep = Annotated[Session, Depends(get_session)]
|
SessionDep = Annotated[Session, Depends(get_session)]
|
||||||
|
|
||||||
|
|
||||||
def get_current_username(token: Annotated[str, Depends(oauth2_scheme)], session: SessionDep) -> str:
|
def get_current_username(token: Annotated[str, Depends(oauth_password_scheme)], session: SessionDep) -> str:
|
||||||
try:
|
try:
|
||||||
payload = jwt.decode(token, settings.SECRET_KEY, algorithms=[settings.ALGORITHM])
|
payload = jwt.decode(token, settings.SECRET_KEY, algorithms=[settings.ALGORITHM])
|
||||||
username = payload.get("sub")
|
username = payload.get("sub")
|
||||||
@ -34,3 +35,12 @@ def get_current_username(token: Annotated[str, Depends(oauth2_scheme)], session:
|
|||||||
if not user:
|
if not user:
|
||||||
raise HTTPException(status_code=401, detail="Invalid Token")
|
raise HTTPException(status_code=401, detail="Invalid Token")
|
||||||
return user.username
|
return user.username
|
||||||
|
|
||||||
|
|
||||||
|
def get_oidc_client():
|
||||||
|
return OAuth2Client(
|
||||||
|
client_id=settings.OIDC_CLIENT_ID,
|
||||||
|
client_secret=settings.OIDC_CLIENT_SECRET,
|
||||||
|
scope="openid",
|
||||||
|
redirect_uri=settings.OIDC_REDIRECT_URI,
|
||||||
|
)
|
||||||
|
|||||||
@ -27,6 +27,11 @@ def _prefix_assets_url(filename: str) -> str:
|
|||||||
return base + filename
|
return base + filename
|
||||||
|
|
||||||
|
|
||||||
|
class AuthParams(BaseModel):
|
||||||
|
oidc: str | None
|
||||||
|
register_enabled: bool
|
||||||
|
|
||||||
|
|
||||||
class TripItemStatusEnum(str, Enum):
|
class TripItemStatusEnum(str, Enum):
|
||||||
PENDING = "pending"
|
PENDING = "pending"
|
||||||
CONFIRMED = "booked"
|
CONFIRMED = "booked"
|
||||||
|
|||||||
@ -1,16 +1,96 @@
|
|||||||
|
import json
|
||||||
|
|
||||||
import jwt
|
import jwt
|
||||||
from fastapi import APIRouter, Body, HTTPException
|
from fastapi import APIRouter, Body, HTTPException
|
||||||
|
from jwt.algorithms import RSAAlgorithm
|
||||||
|
|
||||||
from ..config import settings
|
from ..config import settings
|
||||||
from ..db.core import init_user_data
|
from ..db.core import init_user_data
|
||||||
from ..deps import SessionDep
|
from ..deps import SessionDep, get_oidc_client
|
||||||
from ..models.models import LoginRegisterModel, Token, User
|
from ..models.models import AuthParams, LoginRegisterModel, Token, User
|
||||||
from ..security import (create_access_token, create_tokens, hash_password,
|
from ..security import (create_access_token, create_tokens, hash_password,
|
||||||
verify_password)
|
verify_password)
|
||||||
|
from ..utils.utils import generate_filename, httpx_get
|
||||||
|
|
||||||
router = APIRouter(prefix="/api/auth", tags=["auth"])
|
router = APIRouter(prefix="/api/auth", tags=["auth"])
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/params", response_model=AuthParams)
|
||||||
|
async def auth_params() -> AuthParams:
|
||||||
|
data = {"oidc": None, "register_enabled": settings.REGISTER_ENABLE}
|
||||||
|
|
||||||
|
if settings.OIDC_HOST and settings.OIDC_CLIENT_ID and settings.OIDC_CLIENT_SECRET:
|
||||||
|
oidc_complete_url = f"{settings.OIDC_PROTOCOL}://{settings.OIDC_HOST}/realms/{settings.OIDC_REALM}/protocol/openid-connect/auth?client_id={settings.OIDC_CLIENT_ID}&redirect_uri={settings.OIDC_REDIRECT_URI}&response_type=code&scope=openid"
|
||||||
|
data["oidc"] = oidc_complete_url
|
||||||
|
|
||||||
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/oidc/login", response_model=Token)
|
||||||
|
async def oidc_login(session: SessionDep, code: str = Body(..., embed=True)) -> Token:
|
||||||
|
if settings.AUTH_METHOD != "oidc":
|
||||||
|
raise HTTPException(status_code=400, detail="Bad request")
|
||||||
|
|
||||||
|
try:
|
||||||
|
oidc_client = get_oidc_client()
|
||||||
|
token = oidc_client.fetch_token(
|
||||||
|
f"{settings.OIDC_PROTOCOL}://{settings.OIDC_HOST}/realms/{settings.OIDC_REALM}/protocol/openid-connect/token",
|
||||||
|
grant_type="authorization_code",
|
||||||
|
code=code,
|
||||||
|
)
|
||||||
|
except Exception:
|
||||||
|
raise HTTPException(status_code=401, detail="OIDC login failed")
|
||||||
|
|
||||||
|
id_token = token.get("id_token")
|
||||||
|
alg = jwt.get_unverified_header(id_token).get("alg")
|
||||||
|
|
||||||
|
match alg:
|
||||||
|
case "HS256":
|
||||||
|
decoded = jwt.decode(
|
||||||
|
id_token,
|
||||||
|
settings.OIDC_CLIENT_SECRET,
|
||||||
|
algorithms=alg,
|
||||||
|
audience=settings.OIDC_CLIENT_ID,
|
||||||
|
)
|
||||||
|
case "RS256":
|
||||||
|
config = await httpx_get(
|
||||||
|
f"{settings.OIDC_PROTOCOL}://{settings.OIDC_HOST}/realms/{settings.OIDC_REALM}/.well-known/openid-configuration"
|
||||||
|
)
|
||||||
|
jwks_uri = config.get("jwks_uri")
|
||||||
|
jwks = await httpx_get(jwks_uri)
|
||||||
|
keys = jwks.get("keys")
|
||||||
|
|
||||||
|
for key in keys:
|
||||||
|
try:
|
||||||
|
pk = RSAAlgorithm.from_jwk(json.dumps(key))
|
||||||
|
decoded = jwt.decode(
|
||||||
|
id_token,
|
||||||
|
key=pk,
|
||||||
|
algorithms=alg,
|
||||||
|
audience=settings.OIDC_CLIENT_ID,
|
||||||
|
issuer=f"{settings.OIDC_PROTOCOL}://{settings.OIDC_HOST}/realms/{settings.OIDC_REALM}",
|
||||||
|
)
|
||||||
|
break
|
||||||
|
except Exception:
|
||||||
|
continue
|
||||||
|
case _:
|
||||||
|
raise HTTPException(status_code=500, detail="OIDC login failed, algorithm not handled")
|
||||||
|
|
||||||
|
if not decoded:
|
||||||
|
raise HTTPException(status_code=401, detail="Invalid ID token")
|
||||||
|
|
||||||
|
username = decoded.get("preferred_username")
|
||||||
|
user = session.get(User, username)
|
||||||
|
if not user:
|
||||||
|
# TODO: password is non-null, we must init the pw with something, the model is not made for OIDC
|
||||||
|
user = User(username=username, password=hash_password(generate_filename("find-something-else")))
|
||||||
|
session.add(user)
|
||||||
|
session.commit()
|
||||||
|
init_user_data(session, username)
|
||||||
|
|
||||||
|
return create_tokens(data={"sub": username})
|
||||||
|
|
||||||
|
|
||||||
@router.post("/login", response_model=Token)
|
@router.post("/login", response_model=Token)
|
||||||
def login(req: LoginRegisterModel, session: SessionDep) -> Token:
|
def login(req: LoginRegisterModel, session: SessionDep) -> Token:
|
||||||
db_user = session.get(User, req.username)
|
db_user = session.get(User, req.username)
|
||||||
|
|||||||
@ -36,6 +36,7 @@ def remove_image(path: str):
|
|||||||
try:
|
try:
|
||||||
fpath = Path(assets_folder_path() / path)
|
fpath = Path(assets_folder_path() / path)
|
||||||
if not fpath.exists():
|
if not fpath.exists():
|
||||||
|
# Skips missing file
|
||||||
return
|
return
|
||||||
fpath.unlink()
|
fpath.unlink()
|
||||||
except OSError as exc:
|
except OSError as exc:
|
||||||
@ -51,6 +52,23 @@ def parse_str_or_date_to_date(cdate: str | date) -> date:
|
|||||||
return cdate
|
return cdate
|
||||||
|
|
||||||
|
|
||||||
|
async def httpx_get(link: str) -> str:
|
||||||
|
headers = {
|
||||||
|
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36",
|
||||||
|
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
|
||||||
|
"Accept-Language": "en-US,en;q=0.5",
|
||||||
|
"Referer": link,
|
||||||
|
}
|
||||||
|
|
||||||
|
try:
|
||||||
|
async with httpx.AsyncClient(follow_redirects=True, headers=headers, timeout=5) as client:
|
||||||
|
response = await client.get(link)
|
||||||
|
response.raise_for_status()
|
||||||
|
return response.json()
|
||||||
|
except Exception:
|
||||||
|
raise HTTPException(status_code=400, detail="Bad Request")
|
||||||
|
|
||||||
|
|
||||||
async def download_file(link: str, raise_on_error: bool = False) -> str:
|
async def download_file(link: str, raise_on_error: bool = False) -> str:
|
||||||
headers = {
|
headers = {
|
||||||
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36",
|
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36",
|
||||||
|
|||||||
@ -18,6 +18,13 @@
|
|||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@defer () {
|
||||||
|
@if (authParams?.oidc) {
|
||||||
|
<div class="mt-8 text-center">
|
||||||
|
<p-button variant="outlined" severity="primary" size="large" icon="pi pi-key" label="Sign in"
|
||||||
|
(click)="authenticate()" />
|
||||||
|
</div>
|
||||||
|
} @else {
|
||||||
<div pFocusTrap class="mt-4" [formGroup]="authForm">
|
<div pFocusTrap class="mt-4" [formGroup]="authForm">
|
||||||
<p-floatlabel variant="in">
|
<p-floatlabel variant="in">
|
||||||
<input #username pInputText id="username" formControlName="username" autocorrect="off" autocapitalize="none"
|
<input #username pInputText id="username" formControlName="username" autocorrect="off" autocapitalize="none"
|
||||||
@ -56,6 +63,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@if (authParams?.register_enabled) {
|
||||||
<hr class="my-6 border-gray-300 w-full" />
|
<hr class="my-6 border-gray-300 w-full" />
|
||||||
@if (isRegistering) {
|
@if (isRegistering) {
|
||||||
<p class="mt-8">
|
<p class="mt-8">
|
||||||
@ -70,6 +78,19 @@
|
|||||||
an account</a>
|
an account</a>
|
||||||
</p>
|
</p>
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} @placeholder (minimum 0.4s) {
|
||||||
|
<div class="mt-4">
|
||||||
|
<p-skeleton width="100%" height="3.5rem" />
|
||||||
|
</div>
|
||||||
|
<div class="mt-4">
|
||||||
|
<p-skeleton width="100%" height="3.5rem" />
|
||||||
|
</div>
|
||||||
|
<div class="mt-4 flex justify-end">
|
||||||
|
<p-skeleton width="80px" height="2.5rem" />
|
||||||
|
</div>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="absolute bottom-4 left-1/2 transform -translate-x-1/2 text-sm flex flex-col items-center gap-2">
|
<div class="absolute bottom-4 left-1/2 transform -translate-x-1/2 text-sm flex flex-col items-center gap-2">
|
||||||
@ -85,7 +106,7 @@
|
|||||||
Welcome to TRIP
|
Welcome to TRIP
|
||||||
</div>
|
</div>
|
||||||
<div class="mt-6 text-lg tracking-tight leading-6 text-gray-400">
|
<div class="mt-6 text-lg tracking-tight leading-6 text-gray-400">
|
||||||
Tourism and Recreation Interest Points.
|
Tourism and Recreational Interest Points.
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -1,23 +1,24 @@
|
|||||||
import { Component } from '@angular/core';
|
import { Component } from "@angular/core";
|
||||||
|
|
||||||
import { FloatLabelModule } from 'primeng/floatlabel';
|
import { FloatLabelModule } from "primeng/floatlabel";
|
||||||
import {
|
import {
|
||||||
FormBuilder,
|
FormBuilder,
|
||||||
FormGroup,
|
FormGroup,
|
||||||
FormsModule,
|
FormsModule,
|
||||||
ReactiveFormsModule,
|
ReactiveFormsModule,
|
||||||
Validators,
|
Validators,
|
||||||
} from '@angular/forms';
|
} from "@angular/forms";
|
||||||
import { ActivatedRoute, Router } from '@angular/router';
|
import { ActivatedRoute, Router } from "@angular/router";
|
||||||
import { InputTextModule } from 'primeng/inputtext';
|
import { InputTextModule } from "primeng/inputtext";
|
||||||
import { ButtonModule } from 'primeng/button';
|
import { ButtonModule } from "primeng/button";
|
||||||
import { FocusTrapModule } from 'primeng/focustrap';
|
import { FocusTrapModule } from "primeng/focustrap";
|
||||||
import { AuthService } from '../../services/auth.service';
|
import { AuthParams, AuthService, Token } from "../../services/auth.service";
|
||||||
import { MessageModule } from 'primeng/message';
|
import { MessageModule } from "primeng/message";
|
||||||
import { HttpErrorResponse } from '@angular/common/http';
|
import { HttpErrorResponse } from "@angular/common/http";
|
||||||
|
import { SkeletonModule } from "primeng/skeleton";
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-auth',
|
selector: "app-auth",
|
||||||
standalone: true,
|
standalone: true,
|
||||||
imports: [
|
imports: [
|
||||||
FloatLabelModule,
|
FloatLabelModule,
|
||||||
@ -25,16 +26,18 @@ import { HttpErrorResponse } from '@angular/common/http';
|
|||||||
ButtonModule,
|
ButtonModule,
|
||||||
FormsModule,
|
FormsModule,
|
||||||
InputTextModule,
|
InputTextModule,
|
||||||
|
SkeletonModule,
|
||||||
FocusTrapModule,
|
FocusTrapModule,
|
||||||
MessageModule,
|
MessageModule,
|
||||||
],
|
],
|
||||||
templateUrl: './auth.component.html',
|
templateUrl: "./auth.component.html",
|
||||||
styleUrl: './auth.component.scss',
|
styleUrl: "./auth.component.scss",
|
||||||
})
|
})
|
||||||
export class AuthComponent {
|
export class AuthComponent {
|
||||||
private redirectURL: string;
|
private redirectURL: string;
|
||||||
|
authParams: AuthParams | undefined;
|
||||||
authForm: FormGroup;
|
authForm: FormGroup;
|
||||||
error: string = '';
|
error: string = "";
|
||||||
isRegistering: boolean = false;
|
isRegistering: boolean = false;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@ -43,12 +46,31 @@ export class AuthComponent {
|
|||||||
private route: ActivatedRoute,
|
private route: ActivatedRoute,
|
||||||
private fb: FormBuilder,
|
private fb: FormBuilder,
|
||||||
) {
|
) {
|
||||||
|
this.route.queryParams.subscribe((params) => {
|
||||||
|
const code = params["code"];
|
||||||
|
if (code) {
|
||||||
|
this.authService.oidcLogin(code).subscribe({
|
||||||
|
next: (data) => {
|
||||||
|
if (!data.access_token) {
|
||||||
|
this.error = "Authentication failed";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.router.navigateByUrl(this.redirectURL);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this.authService.authParams().subscribe({
|
||||||
|
next: (params) => (this.authParams = params),
|
||||||
|
});
|
||||||
|
|
||||||
this.redirectURL =
|
this.redirectURL =
|
||||||
this.route.snapshot.queryParams['redirectURL'] || '/home';
|
this.route.snapshot.queryParams["redirectURL"] || "/home";
|
||||||
|
|
||||||
this.authForm = this.fb.group({
|
this.authForm = this.fb.group({
|
||||||
username: ['', { validators: Validators.required }],
|
username: ["", { validators: Validators.required }],
|
||||||
password: ['', { validators: Validators.required }],
|
password: ["", { validators: Validators.required }],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -58,7 +80,7 @@ export class AuthComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
register(): void {
|
register(): void {
|
||||||
this.error = '';
|
this.error = "";
|
||||||
if (this.authForm.valid) {
|
if (this.authForm.valid) {
|
||||||
this.authService.register(this.authForm.value).subscribe({
|
this.authService.register(this.authForm.value).subscribe({
|
||||||
next: () => {
|
next: () => {
|
||||||
@ -73,17 +95,22 @@ export class AuthComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
authenticate(): void {
|
authenticate(): void {
|
||||||
this.error = '';
|
this.error = "";
|
||||||
if (this.authForm.valid) {
|
if (this.authParams?.oidc) {
|
||||||
this.authService.login(this.authForm.value).subscribe({
|
window.location.replace(encodeURI(this.authParams.oidc));
|
||||||
next: () => {
|
|
||||||
this.router.navigateByUrl(this.redirectURL);
|
|
||||||
},
|
|
||||||
error: (err: HttpErrorResponse) => {
|
|
||||||
this.authForm.reset();
|
|
||||||
this.error = err.error.detail;
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.authService.login(this.authForm.value).subscribe({
|
||||||
|
next: (data) => {
|
||||||
|
if (!data.access_token) {
|
||||||
|
this.error = "Authentication failed";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.router.navigateByUrl(this.redirectURL);
|
||||||
|
},
|
||||||
|
error: () => {
|
||||||
|
this.authForm.reset();
|
||||||
|
},
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -11,6 +11,11 @@ export interface Token {
|
|||||||
access_token: string;
|
access_token: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface AuthParams {
|
||||||
|
register_enabled: boolean;
|
||||||
|
oidc?: string;
|
||||||
|
}
|
||||||
|
|
||||||
const JWT_TOKEN = "TRIP_AT";
|
const JWT_TOKEN = "TRIP_AT";
|
||||||
const REFRESH_TOKEN = "TRIP_RT";
|
const REFRESH_TOKEN = "TRIP_RT";
|
||||||
const JWT_USER = "TRIP_USER";
|
const JWT_USER = "TRIP_USER";
|
||||||
@ -23,7 +28,7 @@ export class AuthService {
|
|||||||
private httpClient: HttpClient,
|
private httpClient: HttpClient,
|
||||||
private router: Router,
|
private router: Router,
|
||||||
private apiService: ApiService,
|
private apiService: ApiService,
|
||||||
private utilsService: UtilsService
|
private utilsService: UtilsService,
|
||||||
) {
|
) {
|
||||||
this.apiBaseUrl = this.apiService.apiBaseUrl;
|
this.apiBaseUrl = this.apiService.apiBaseUrl;
|
||||||
}
|
}
|
||||||
@ -52,6 +57,10 @@ export class AuthService {
|
|||||||
return localStorage.getItem(REFRESH_TOKEN) ?? "";
|
return localStorage.getItem(REFRESH_TOKEN) ?? "";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
authParams(): Observable<AuthParams> {
|
||||||
|
return this.httpClient.get<AuthParams>(this.apiBaseUrl + "/auth/params");
|
||||||
|
}
|
||||||
|
|
||||||
storeTokens(tokens: Token): void {
|
storeTokens(tokens: Token): void {
|
||||||
this.accessToken = tokens.access_token;
|
this.accessToken = tokens.access_token;
|
||||||
this.refreshToken = tokens.refresh_token;
|
this.refreshToken = tokens.refresh_token;
|
||||||
@ -71,26 +80,46 @@ export class AuthService {
|
|||||||
.pipe(
|
.pipe(
|
||||||
tap((tokens: Token) => {
|
tap((tokens: Token) => {
|
||||||
this.accessToken = tokens.access_token;
|
this.accessToken = tokens.access_token;
|
||||||
})
|
}),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
login(authForm: { username: string; password: string }): Observable<Token> {
|
login(authForm: { username: string; password: string }): Observable<Token> {
|
||||||
return this.httpClient.post<Token>(this.apiBaseUrl + "/auth/login", authForm).pipe(
|
return this.httpClient
|
||||||
tap((tokens: Token) => {
|
.post<Token>(this.apiBaseUrl + "/auth/login", authForm)
|
||||||
this.loggedUser = authForm.username;
|
.pipe(
|
||||||
this.storeTokens(tokens);
|
tap((tokens: Token) => {
|
||||||
})
|
this.loggedUser = authForm.username;
|
||||||
);
|
this.storeTokens(tokens);
|
||||||
|
}),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
register(authForm: { username: string; password: string }): Observable<Token> {
|
register(authForm: {
|
||||||
return this.httpClient.post<Token>(this.apiBaseUrl + "/auth/register", authForm).pipe(
|
username: string;
|
||||||
tap((tokens: Token) => {
|
password: string;
|
||||||
this.loggedUser = authForm.username;
|
}): Observable<Token> {
|
||||||
this.storeTokens(tokens);
|
return this.httpClient
|
||||||
})
|
.post<Token>(this.apiBaseUrl + "/auth/register", authForm)
|
||||||
);
|
.pipe(
|
||||||
|
tap((tokens: Token) => {
|
||||||
|
this.loggedUser = authForm.username;
|
||||||
|
this.storeTokens(tokens);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
oidcLogin(code: string): Observable<Token> {
|
||||||
|
return this.httpClient
|
||||||
|
.post<Token>(this.apiBaseUrl + "/auth/oidc/login", { code })
|
||||||
|
.pipe(
|
||||||
|
tap((data: any) => {
|
||||||
|
if (data.access_token && data.refresh_token) {
|
||||||
|
this.loggedUser = this._getTokenUsername(data.access_token);
|
||||||
|
this.storeTokens(data);
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
logout(custom_msg: string = "", is_error = false): void {
|
logout(custom_msg: string = "", is_error = false): void {
|
||||||
@ -99,7 +128,11 @@ export class AuthService {
|
|||||||
|
|
||||||
if (custom_msg) {
|
if (custom_msg) {
|
||||||
if (is_error) {
|
if (is_error) {
|
||||||
this.utilsService.toast("error", "You must be authenticated", custom_msg);
|
this.utilsService.toast(
|
||||||
|
"error",
|
||||||
|
"You must be authenticated",
|
||||||
|
custom_msg,
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
this.utilsService.toast("success", "Success", custom_msg);
|
this.utilsService.toast("success", "Success", custom_msg);
|
||||||
}
|
}
|
||||||
@ -135,19 +168,25 @@ export class AuthService {
|
|||||||
private _b64DecodeUnicode(str: any): string {
|
private _b64DecodeUnicode(str: any): string {
|
||||||
return decodeURIComponent(
|
return decodeURIComponent(
|
||||||
Array.prototype.map
|
Array.prototype.map
|
||||||
.call(this._b64decode(str), (c: any) => "%" + ("00" + c.charCodeAt(0).toString(16)).slice(-2))
|
.call(
|
||||||
.join("")
|
this._b64decode(str),
|
||||||
|
(c: any) => "%" + ("00" + c.charCodeAt(0).toString(16)).slice(-2),
|
||||||
|
)
|
||||||
|
.join(""),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private _b64decode(str: string): string {
|
private _b64decode(str: string): string {
|
||||||
const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=";
|
const chars =
|
||||||
|
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=";
|
||||||
let output = "";
|
let output = "";
|
||||||
|
|
||||||
str = String(str).replace(/=+$/, "");
|
str = String(str).replace(/=+$/, "");
|
||||||
|
|
||||||
if (str.length % 4 === 1) {
|
if (str.length % 4 === 1) {
|
||||||
throw new Error("'atob' failed: The string to be decoded is not correctly encoded.");
|
throw new Error(
|
||||||
|
"'atob' failed: The string to be decoded is not correctly encoded.",
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* eslint-disable */
|
/* eslint-disable */
|
||||||
@ -186,6 +225,20 @@ export class AuthService {
|
|||||||
return this._b64DecodeUnicode(output);
|
return this._b64DecodeUnicode(output);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private _getTokenUsername(token: string): string {
|
||||||
|
const decodedToken = this._decodeToken(token);
|
||||||
|
|
||||||
|
if (decodedToken === null) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!decodedToken.hasOwnProperty("sub")) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
return decodedToken.sub;
|
||||||
|
}
|
||||||
|
|
||||||
private _decodeToken(token: string): any {
|
private _decodeToken(token: string): any {
|
||||||
if (!token) {
|
if (!token) {
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user