Compare commits

...

10 Commits

Author SHA1 Message Date
Bastian Wagner
fd216edf50 Indikator für Fehlerhafte inputs eingebaut
Some checks failed
Docker Image CI for GHCR / ssh-login-and-publish (push) Has been cancelled
2026-02-18 11:14:24 +01:00
Bastian Wagner
990d268460 auslieferung 2026-02-18 11:01:29 +01:00
Bastian Wagner
fe27c6f918 api für register 2026-02-18 10:50:43 +01:00
Bastian Wagner
afed523d5b queryparameter 2024-12-27 21:56:01 +01:00
Bastian Wagner
40cd25a771 url fix 2024-09-12 17:54:36 +02:00
Bastian Wagner
3bff98503e fix 2024-09-12 15:54:30 +02:00
Bastian Wagner
f9b151d914 fix login 2024-09-12 14:01:53 +02:00
Bastian Wagner
abd623f2ca pw revision 2024-09-12 13:47:56 +02:00
Bastian Wagner
ec29f8d4b1 Invalidate old Token 2024-09-12 13:26:54 +02:00
Bastian Wagner
2362f04704 URL angepasst 2024-09-12 09:50:57 +02:00
24 changed files with 225 additions and 24 deletions

View File

@@ -23,5 +23,6 @@ MAILER_PASSWORD=xxxxx
MAILER_FROM='"No Reply" <noreply@example.com>'
# Client
# Url wird für Mailversand gebraucht
CLIENT_URL=http://localhost:4200
PRODUCTION=true

View File

@@ -12,7 +12,7 @@ export class ApplicationController {
return { success: true };
}
@Post('login')
@Post('authorize')
loginUser(@Body() b: LoginUserDto): Promise<User> {
return this.userService.loginUser({
username: b.username,

View File

@@ -7,7 +7,7 @@ import { Client } from 'src/model';
import { UsersService } from 'src/shared/users.service';
import { CustomLogger } from 'src/shared/logger/custom.logger';
@Controller('auth')
@Controller('')
export class AuthController {
constructor(
private usersService: UsersService,
@@ -30,7 +30,7 @@ export class AuthController {
async login(
@Body('username') username: string,
@Body('password') password: string,
@Body('client_id') clientId: string,
@Query('client_id') clientId: string,
) {
const token = await this.usersService.createToken(
username,
@@ -57,7 +57,7 @@ export class AuthController {
return this.usersService.loginWithSessionKey(code, null, true);
}
@Get()
@Get('client')
async getClient(
@Query('client_id') clientId: string,
@Query('response_type') responseType: string,
@@ -72,7 +72,7 @@ export class AuthController {
);
}
@Post('token')
@Post('authorize')
@FormDataRequest()
async getToken(
@Body('client_id') clientId: string,

View File

@@ -5,9 +5,12 @@ import {
DataSource,
Repository,
CreateDateColumn,
Column,
BeforeInsert,
} from 'typeorm';
import { User } from './user.entity';
import { Injectable } from '@nestjs/common';
import { Exclude } from 'class-transformer';
@Entity()
export class SessionKey {
@@ -17,8 +20,17 @@ export class SessionKey {
@ManyToOne(() => User, (user) => user.sessionKeys, { eager: true })
user: User;
@Exclude()
@Column()
pwRevision?: number;
@CreateDateColumn()
createdAt: Date;
@BeforeInsert()
setPWRevision() {
this.pwRevision = this.user.pwRevision;
}
}
@Injectable()

View File

@@ -57,6 +57,10 @@ export class User {
@ManyToMany(() => Client, (client) => client.admins)
clients?: Client[];
@Exclude()
@Column({ type: 'int', default: 0 })
pwRevision?: number; // wird hochgezählt wenn das PW geändert wird. somit kann der Token invalid gesetzt werden.
accessToken?: string;
refreshToken?: string;
session_key?: string;

View File

@@ -1,3 +1,4 @@
export * from './authenticated.request';
export * from './logger.interface';
export * from './mailconfig.interface';
export * from './payload.interface';

View File

@@ -0,0 +1,18 @@
export interface IAccessPayload {
username: string;
firstName: string;
lastName: string;
id: string;
iss: string;
aud: string;
iat: number;
exp: number;
}
export interface IRefreshPayload {
type: string;
id: string;
token_revision: number;
iat: number;
exp: number;
}

View File

@@ -15,6 +15,8 @@ import {
AuthorizationCodeRepository,
AuthorizationCode,
ActivityLogRepository,
IAccessPayload,
IRefreshPayload,
} from 'src/model';
import { CustomLogger } from './logger/custom.logger';
@@ -123,6 +125,10 @@ export class UsersService {
throw new HttpException('User is not active', 401);
}
if (user.pwRevision != session.pwRevision) {
throw new HttpException('Invalid session key', 401);
}
if (getUserAccessToken) {
user.accessToken = this.createAccessToken(user);
user.refreshToken = this.createRefreshToken(user);
@@ -205,25 +211,30 @@ export class UsersService {
{
type: 'refresh',
id: user.id,
token_revision: user.pwRevision,
},
{ expiresIn: '365d' },
);
}
async getNewAccessToken(refreshToken: string) {
const payload = this.jwtService.verify(refreshToken);
const payload: IRefreshPayload = this.jwtService.verify(refreshToken);
if (payload.type !== 'refresh') {
this.logger.error(`Token ${refreshToken} is not a refresh token`);
throw new HttpException('Invalid token', 401);
}
const user = await this.userRepo.findById(payload.id);
if (!user) {
if (
!user ||
payload['token_revision'] == undefined ||
payload['token_revision'] != user.pwRevision
) {
this.logger.error(`User ${payload.id} not found for refresh token`);
throw new HttpException('Invalid token', 401);
}
const token = this.createAccessToken(user);
const pay = this.jwtService.decode(token);
const pay: IAccessPayload = this.jwtService.decode(token);
const result = {
access_token: token,
expires_in: pay.exp - pay.iat,
@@ -237,6 +248,7 @@ export class UsersService {
try {
const decoded = this.jwtService.verify(token);
this.activityRepo.logAccessTokenVerification();
console.log(decoded, '-');
return decoded;
} catch (e) {
this.logger.error(`Token ${token} is invalid. Error: ${e.message}`);
@@ -303,6 +315,7 @@ export class UsersService {
if (savedCode && savedCode.user) {
const hashedPassword = await bcrypt.hash(dto.password, 10);
savedCode.user.password = hashedPassword;
savedCode.user.pwRevision += 1;
await this.userRepo.save(savedCode.user);
await this.resetPwRepo.remove(savedCode);
await this.sessionRepo.delete({ user: { id: savedCode.user.id } });

View File

@@ -7,6 +7,7 @@ import { ResetPwComponent } from './auth/reset-pw/reset-pw.component';
export const routes: Routes = [
{ path: 'login', component: LoginComponent, canActivate: [SessionKeyGuard] },
{ path: 'authorize', component: LoginComponent, canActivate: [SessionKeyGuard] },
{ path: 'register', component: RegisterComponent },
{ path: 'pw-reset', component: ResetPwComponent },
{ path: 'dashboard', component: DashboardComponent, canActivate: [SessionKeyGuard] },

View File

@@ -125,6 +125,9 @@ body {
outline: none;
border-bottom-color: #6A679E;
}
.login__input_error {
border-bottom: 2px solid #b61d09;
}
.login__submit {
background: #fff;

View File

@@ -66,8 +66,8 @@ export class LoginComponent {
this.client_id = params.client_id;
if (!this.client_id) { return; }
this.http.get<any>('api/auth/', {
console.log(params);
this.http.get<any>('api/client', {
params
}).subscribe({
next: (client) => {
@@ -82,7 +82,7 @@ export class LoginComponent {
login() {
this.isLoading = true;
const url = this.client_id ? `api/auth/login?client_id=${this.client_id}` : 'api/app/login';
const url = this.client_id ? `api/login?client_id=${this.client_id}` : 'api/app/authorize';
console.log(url, this.client_id)
this.http.post(url, this.loginForm.value).
pipe(

View File

@@ -13,15 +13,15 @@
</div>
<div class="login__field">
<i class="login__icon fas fa-user user"></i>
<input formControlName="username" type="text" class="login__input" autocomplete="email" placeholder="User name / Email">
<input formControlName="username" type="text" class="login__input" autocomplete="email" placeholder="User name / Email" [class.login__input_error]="registerForm.controls.username.touched && registerForm.controls.username.invalid">
</div>
<div class="login__field">
<i class="login__icon fas fa-lock safe"></i>
<input type="password" formControlName="password" autocomplete="new-password" class="login__input" placeholder="Password">
<input type="password" formControlName="password" autocomplete="new-password" class="login__input" placeholder="Password" [class.login__input_error]="registerForm.controls.password.touched && registerForm.controls.password.invalid">
</div>
<div class="login__field">
<i class="login__icon fas fa-lock safe"></i>
<input type="password" formControlName="repeatPassword" autocomplete="new-password" class="login__input" placeholder="Repeat password">
<input type="password" formControlName="repeatPassword" autocomplete="new-password" class="login__input" placeholder="Repeat password" [class.login__input_error]="registerForm.controls.repeatPassword.touched && registerForm.controls.repeatPassword.invalid">
</div>
<button class="button login__submit" (click)="register()" [disabled]="!client_id || registerForm.invalid">
<span class="button__text">Register</span>

View File

@@ -24,8 +24,8 @@ export class RegisterComponent {
registerForm = new FormGroup({
username: new FormControl('', [Validators.required, Validators.email, Validators.maxLength(100)]),
password: new FormControl('', [Validators.required, Validators.minLength(6), Validators.maxLength(20)]),
repeatPassword: new FormControl('', [Validators.required, Validators.minLength(6), Validators.maxLength(20)]),
password: new FormControl('', [Validators.required, Validators.minLength(6), Validators.maxLength(64)]),
repeatPassword: new FormControl('', [Validators.required, Validators.minLength(6), Validators.maxLength(64)]),
firstName: new FormControl('', [Validators.required, Validators.maxLength(100)]),
lastName: new FormControl('', [Validators.required, Validators.maxLength(100)]),
})
@@ -39,7 +39,7 @@ export class RegisterComponent {
const params = (this.route.snapshot.queryParamMap as any)["params"];
this.redirectUri = params.redirect_uri;
this.client_id = params.client_id;
this.http.get<any>('api/auth/', {
this.http.get<any>('api/client', {
params
}).subscribe({
next: (client) => {
@@ -57,7 +57,7 @@ export class RegisterComponent {
this.toast.error('Passwords do not match');
return;
}
this.http.post('api/auth/register?'+ 'client_id=' + this.client_id, this.registerForm.value).pipe(
this.http.post('api/register?'+ 'client_id=' + this.client_id, this.registerForm.value).pipe(
this.toast.observe({
loading: 'Registering...',
success: 'Registration successfull, please log in',

View File

@@ -38,7 +38,7 @@ export class ResetPwComponent {
resetPassword() {
this.http.post('api/auth/reset', this.resetPw.value)
this.http.post('api/reset', this.resetPw.value)
.pipe(
this.toast.observe({
loading: 'Sende Mail...',
@@ -59,7 +59,7 @@ export class ResetPwComponent {
this.toast.error('Die Passwörter stimmen nicht überein');
return;
}
this.http.post('api/auth/reset', this.setNewPwForm.value)
this.http.post('api/reset', this.setNewPwForm.value)
.pipe(
this.toast.observe({
loading: 'Setze neues Passwort',

View File

@@ -40,7 +40,7 @@ export class SessionKeyGuard {
const id = window.localStorage.getItem("auth_session_key");
if (!id ||id.length < 2) { return resolve(true); }
const url = this.client_id ? 'api/auth/login-with-session-id' : 'api/auth/login-with-session-id/userlogin'
const url = this.client_id ? 'api/login-with-session-id' : 'api/login-with-session-id/userlogin'
this.http.post(url, {
code: id,

View File

@@ -0,0 +1,92 @@
<div mat-dialog-title>Hilfe</div>
<mat-dialog-content>
<div class="mat-body">
<h3>Authentifizierung:</h3>
<div>Für den Login:</div>
<code>https://sso.beantastic.de/authorize</code>
<h5>Query:</h5>
<table>
<tr>
<td>response_type</td>
<td>code</td>
</tr>
<tr>
<td>client_id</td>
<td>&lt;CLIENT ID&gt;</td>
</tr>
<tr>
<td>redirect_uri</td>
<td>&lt;REDIRECT URI&gt;</td>
</tr>
<tr>
<td>scope</td>
<td>&lt;SCOPE&gt;</td>
</tr>
</table>
<br />
<div>
Danach wird der user mit einem auth code als parameter <code>?code=<b>&lt;AUTH_CODE&gt;</b></code> zurückgeleitet.
</div>
<h3>Code => Accesstoken:</h3>
<div>den Code tauscht der Client gegen den Accesstoken:</div>
<code>POST: https://sso.beantastic.de/api/authorize</code>
<h5>Body (Form):</h5>
<table>
<tr>
<td>client_id</td>
<td>&lt;CLIENT ID&gt;</td>
</tr>
<tr>
<td>client_secret</td>
<td>&lt;Secret&gt;</td>
</tr>
<tr>
<td>code</td>
<td>&lt;Auth Code&gt;</td>
</tr>
<tr>
<td>grant_type</td>
<td>authorization code</td>
</tr>
</table>
<h3>Accesstoken prüfen:</h3>
<div>Einen Accesstoken verifizieren:</div>
<code>POST: https://sso.beantastic.de/api/verify</code>
<h5>Body (Form):</h5>
<table>
<tr>
<td>access_token</td>
<td>&lt;Access Token&gt;</td>
</tr>
</table>
<h5>Return:</h5>
Decoded Token
<h3>neuen Accesstoken:</h3>
<div>um einen Refreshtoken in einen Accesstoken zu tauschen:</div>
<code>POST: https://sso.beantastic.de/api/authorize</code>
<h5>Body (Form):</h5>
<table>
<tr>
<td>client_id</td>
<td>&lt;CLIENT_ID&gt;</td>
</tr>
<tr>
<td>code</td>
<td>&lt;Refresh Token&gt;</td>
</tr>
<tr>
<td>grant_type</td>
<td>refreshtoken</td>
</tr>
</table>
</div>
</mat-dialog-content>
<mat-dialog-actions>
<button mat-button mat-dialog-close >Schließen</button>
</mat-dialog-actions>

View File

@@ -0,0 +1,7 @@
h1, h2, h3, h4, h5 {
margin-bottom: 0;
}
h5 {
margin-top: 8px;
}

View File

@@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { HelpComponent } from './help.component';
describe('HelpComponent', () => {
let component: HelpComponent;
let fixture: ComponentFixture<HelpComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [HelpComponent]
})
.compileComponents();
fixture = TestBed.createComponent(HelpComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -0,0 +1,14 @@
import { Component } from '@angular/core';
import { MatButtonModule } from '@angular/material/button';
import { MatDialogModule } from '@angular/material/dialog';
@Component({
selector: 'app-help',
standalone: true,
imports: [MatDialogModule, MatButtonModule],
templateUrl: './help.component.html',
styleUrl: './help.component.scss'
})
export class HelpComponent {
}

View File

@@ -1,8 +1,11 @@
<div class="header">
<div class="title">SSO Beantastic</div>
<div class="flex-row">
<div class="question" (click)="openHelp()"></div>
<div> {{ userName }}</div>
<div class="logout" (click)="logout()" ></div>
<div class="logout" (click)="logout()" ></div>
</div>
</div>

View File

@@ -63,7 +63,7 @@
justify-content: center;
}
.logout{
.logout, .question {
width: 32px;
height: 32px;
cursor: pointer;

View File

@@ -11,6 +11,7 @@ import { CreateClientComponent } from './components/create-client/create-client.
import { CreateHotToastRef, HotToastService } from '@ngxpert/hot-toast';
import {MatBottomSheet, MatBottomSheetModule, MatBottomSheetRef} from '@angular/material/bottom-sheet';
import { LoginChartComponent } from './components/charts/login/login.chart.component';
import { HelpComponent } from './components/help/help.component';
@Component({
selector: 'app-dashboard',
@@ -38,7 +39,6 @@ export class DashboardComponent implements OnInit {
this.router.navigateByUrl("/login");
return;
}
this.load();
}
@@ -123,6 +123,10 @@ export class DashboardComponent implements OnInit {
logout() {
this.userService.logout();
}
openHelp() {
this.dialog.open(HelpComponent)
}
}

View File

@@ -0,0 +1 @@
<svg enable-background="new 0 0 512 512" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg"><g id="_x32_9_question"><g><path d="m72.632 129.761h178.052c2.702 0 4.888-2.186 4.888-4.888s-2.186-4.888-4.888-4.888h-178.052c-2.702 0-4.888 2.186-4.888 4.888s2.186 4.888 4.888 4.888z"/><path d="m72.632 177.873h108.583c2.702 0 4.888-2.186 4.888-4.888s-2.186-4.888-4.888-4.888h-108.583c-2.702 0-4.888 2.186-4.888 4.888s2.186 4.888 4.888 4.888z"/><path d="m72.632 225.984h217.16c2.702 0 4.888-2.186 4.888-4.888s-2.186-4.888-4.888-4.888h-217.16c-2.702 0-4.888 2.186-4.888 4.888-.001 2.702 2.186 4.888 4.888 4.888z"/><path d="m72.632 274.091h217.16c2.702 0 4.888-2.186 4.888-4.888s-2.186-4.888-4.888-4.888h-217.16c-2.702 0-4.888 2.186-4.888 4.888s2.186 4.888 4.888 4.888z"/><path d="m210.034 312.425h-137.402c-2.702 0-4.888 2.186-4.888 4.888s2.186 4.888 4.888 4.888h137.403c2.702 0 4.888-2.186 4.888-4.888s-2.187-4.888-4.889-4.888z"/><path d="m210.034 360.532h-137.402c-2.702 0-4.888 2.186-4.888 4.888s2.186 4.888 4.888 4.888h137.403c2.702 0 4.888-2.186 4.888-4.888s-2.187-4.888-4.889-4.888z"/><path d="m379.756 269.501v-165.91c0-1.047-.532-2.599-1.461-3.509l-92.685-90.829c-.881-.863-2.384-1.389-3.461-1.389h-216.754c-20.165 0-36.568 16.403-36.568 36.568v379.788c0 20.165 16.403 36.568 36.568 36.568h208.864c21.614 26.436 54.462 43.347 91.195 43.347 64.911 0 117.72-52.828 117.72-117.768 0-60.092-45.232-109.779-103.418-116.866zm-92.68-245.078 75.799 74.26h-66.562c-5.094 0-9.238-4.168-9.238-9.29v-64.97zm-221.681 426.589c-14.77 0-26.791-12.021-26.791-26.791v-379.789c0-14.77 12.021-26.791 26.791-26.791h211.904v71.752c0 10.512 8.531 19.067 19.014 19.067h73.666v160.256c-1.504-.057-3.008-.115-4.526-.115-64.939 0-117.768 52.828-117.768 117.768 0 23.861 7.153 46.073 19.397 64.643zm300.059 43.347c-59.545 0-107.991-48.446-107.991-107.991s48.446-107.991 107.991-107.991c59.521 0 107.943 48.446 107.943 107.991s-48.422 107.991-107.943 107.991z"/><path d="m367.354 319.553h-3.848c-19.707 0-35.737 16.036-35.737 35.742 0 2.702 2.186 4.888 4.888 4.888s4.888-2.186 4.888-4.888c0-14.317 11.644-25.965 25.961-25.965h3.848c14.317 0 25.965 11.648 25.965 25.965 0 28.204-25.253 36.571-26.419 37.571-8.24 4.187-13.362 12.522-13.362 21.764v11.534c0 2.702 2.186 4.888 4.888 4.888s4.888-2.186 4.888-4.888v-11.534c0-5.538 3.07-10.536 8.011-13.047 1.123-.978 31.77-11.54 31.77-46.288.001-19.707-16.035-35.742-35.741-35.742z"/><path d="m358.36 440.514c-2.702 0-4.888 2.186-4.888 4.888v2.893c0 2.702 2.186 4.888 4.888 4.888s4.888-2.186 4.888-4.888v-2.893c0-2.702-2.186-4.888-4.888-4.888z"/></g></g><g id="Layer_1"/></svg>

After

Width:  |  Height:  |  Size: 2.5 KiB

View File

@@ -20,6 +20,10 @@ html, body {
background-image: url("assets/icons/logout.svg");
}
.question {
background-image: url("assets/icons/question.svg");
}
.flex-row{
display: flex;
flex-direction: row;