Skip to content

Commit 645c73f

Browse files
gflachsastanik
andauthored
Enhance Authentication with JWT and Refresh Token Mechanism #264 (#288)
Co-authored-by: Alexander Stanik <astanik@users.noreply.github.com>
1 parent 6b14afb commit 645c73f

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

43 files changed

+3578
-2174
lines changed

README.md

+46-15
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
![GitHub Release](https://img.shields.io/github/v/release/remsfal/remsfal-backend?label=latest%20release)
44
![Build Status](https://img.shields.io/badge/build-passing-brightgreen)
55
[![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=remsfal_remsfal-backend&metric=alert_status)](https://sonarcloud.io/summary/new_code?id=remsfal_remsfal-backend)
6-
![Contributors](https://img.shields.io/github/contributors/remsfal/remsfal-backend)
6+
![Contributors](https://img.shields.io/github/contributors/remsfal/remsfal-backend)
77

88
# Open Source Facility Management Software (Backend)
99

@@ -12,11 +12,12 @@ It works together with the [`remsfal-frontend`](https://github.com/remsfal/remsf
1212
You can see a live version at https://remsfal.de.
1313

1414
## Prerequisits
15-
You will need
16-
- Java 17 or higher
17-
- Maven 3.8.1 or higher
18-
- a **mysql** database
1915

16+
You will need
17+
18+
- Java 17 or higher
19+
- Maven 3.8.1 or higher
20+
- a **mysql** database
2021

2122
## How to get started
2223

@@ -27,17 +28,23 @@ docker compose up -d
2728
```
2829

2930
### Configuration
30-
Furthermore you will need to configurate at least the [database](#database) and [Google Oauth](#google-oauth) to run the application in [application.properties](remsfal-service/src/main/resources/application.properties) or specific them directly as JVM argument.
31+
32+
Furthermore you will need to configurate at least the [database](#database) and [Google Oauth](#google-oauth) to run the
33+
application in [application.properties](remsfal-service/src/main/resources/application.properties) or specific them
34+
directly as JVM argument.
3135

3236
#### database
37+
3338
Adjust the configuration for your database, don't use the provided ones in production!
39+
3440
```properties
3541
quarkus.datasource.username=root
3642
quarkus.datasource.jdbc.url=jdbc:mysql://localhost:3306/REMSFAL
3743
quarkus.datasource.devservices.enabled=false
3844
```
3945

4046
Or use JVM agruments
47+
4148
```sh
4249
java -Dquarkus.datasource.username=root \
4350
-Dquarkus.datasource.jdbc.url=jdbc:mysql://localhost:3306/REMSFAL \
@@ -46,62 +53,86 @@ java -Dquarkus.datasource.username=root \
4653
```
4754

4855
#### Google OAuth
49-
You will also need to provide your own secrets for [Google OAuth](https://developers.google.com/identity/protocols/oauth2?hl=de). As mentioned before you may also use JVM arguments.
56+
57+
You will also need to provide your own secrets
58+
for [Google OAuth](https://developers.google.com/identity/protocols/oauth2?hl=de). As mentioned before you may also use
59+
JVM arguments.
60+
5061
```properties
5162
de.remsfal.auth.oidc.client-id=<YOUR-ID>.apps.googleusercontent.com
5263
de.remsfal.auth.oidc.client-secret=<YOUR-SECRET>
5364
de.remsfal.auth.session.secret=<YOUR-CUSTOM-SESSION-SECRET>
5465
```
5566

56-
#### Run
67+
## JWT Token
68+
69+
For the JWT token its highly recommended to replace the default private key and public key with your own.
70+
71+
### Generate new keys in PEM format using openssl
72+
73+
```sh
74+
openssl genrsa -out private.pem 2048
75+
openssl rsa -in private.pem -pubout -out public.pem
76+
```
77+
78+
#### Run
5779

5880
To package and execute the application
81+
5982
```sh
6083
./mvnw package
6184
java -jar remsfal-service/target/remsfal-service-runner.jar
6285
```
6386

6487
After execution `remsfal-backend` will be available under [`https://localhost:8080/api`](https://localhost:8080/api).
6588

66-
6789
## Contributing
68-
When contributing to this repository, please **first** discuss the change you wish to make by creating an issue before making a change.
90+
91+
When contributing to this repository, please **first** discuss the change you wish to make by creating an issue before
92+
making a change.
6993

7094
Once you got feedback on your idea feel free to fork the project and open a pull request.
7195

7296
Please only make changes in files directly related to your issue.
7397

74-
This project uses [Checkstyle](https://github.com/checkstyle/checkstyle) for code formatting. Please ensure your code adheres to the style defined in the [checkstyle.xml](src/main/style/checkstyle.xml).
98+
This project uses [Checkstyle](https://github.com/checkstyle/checkstyle) for code formatting. Please ensure your code
99+
adheres to the style defined in the [checkstyle.xml](src/main/style/checkstyle.xml).
75100

76101
### CI/CD
77-
This project utilizes Github Actions to check the code quality using [SonarCloud](https://sonarcloud.io/summary/new_code?id=remsfal_remsfal-backend&branch=main) therefore its mandatory to pass the specified **Quality Gates** before a pull request can be merged.
78102

103+
This project utilizes Github Actions to check the code quality
104+
using [SonarCloud](https://sonarcloud.io/summary/new_code?id=remsfal_remsfal-backend&branch=main) therefore its
105+
mandatory to pass the specified **Quality Gates** before a pull request can be merged.
79106

80107
### Development
81108

82109
The project is structured into multiple modules:
83110

84111
**remsfal-core**: Contains core business logic and API interfaces.
85-
**remsfal-service**: Implements the REST API and application services.
86-
112+
**remsfal-service**: Implements the REST API and application services.
87113

88114
At first you well need to start the db as described in [Prerequisits](#prerequisits).
89115

90116
Next run the project using the following command:
117+
91118
```sh
92119
./mvnw clean install
93120
./mvnw compile quarkus:dev -pl remsfal-service
94121
```
122+
95123
It will automatically recompile when you change something.
96124

97125
### Stylecheck
98126

99127
To run the stylecheck use the following command:
128+
100129
```sh
101130
./mvnw checkstyle:checkstyle
102131
```
103132

104133
## Copyright
134+
105135
All licenses in this repository are copyrighted by their respective authors.
106-
Everything else is released under Apache 2.0. See [LICENSE](https://github.com/remsfal/remsfal-backend?tab=Apache-2.0-1-ov-file#readme) for details.
136+
Everything else is released under Apache 2.0.
137+
See [LICENSE](https://github.com/remsfal/remsfal-backend?tab=Apache-2.0-1-ov-file#readme) for details.
107138

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
package de.remsfal.core.model;
2+
3+
public interface UserAuthenticationModel {
4+
UserModel getUser();
5+
String getRefreshToken();
6+
}

remsfal-service/pom.xml

+4
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,10 @@
148148
<artifactId>quarkus-jacoco</artifactId>
149149
<scope>test</scope>
150150
</dependency>
151+
<dependency>
152+
<groupId>io.quarkus</groupId>
153+
<artifactId>quarkus-smallrye-jwt</artifactId>
154+
</dependency>
151155
</dependencies>
152156
<build>
153157
<finalName>remsfal-service</finalName>

remsfal-service/src/main/java/de/remsfal/service/boundary/AuthenticationResource.java

+31-40
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,28 @@
11
package de.remsfal.service.boundary;
22

3+
import com.google.api.client.googleapis.auth.oauth2.GoogleIdToken;
4+
import com.google.api.client.googleapis.auth.oauth2.GoogleIdToken.Payload;
5+
import de.remsfal.core.api.AuthenticationEndpoint;
6+
import de.remsfal.core.model.UserModel;
7+
import de.remsfal.service.boundary.authentication.GoogleAuthenticator;
8+
import de.remsfal.service.boundary.authentication.SessionManager;
9+
import de.remsfal.service.boundary.exception.UnauthorizedException;
10+
import de.remsfal.service.control.UserController;
311
import jakarta.inject.Inject;
412
import jakarta.ws.rs.ForbiddenException;
513
import jakarta.ws.rs.core.Context;
614
import jakarta.ws.rs.core.HttpHeaders;
15+
import jakarta.ws.rs.core.NewCookie;
716
import jakarta.ws.rs.core.Response;
817
import jakarta.ws.rs.core.UriBuilder;
918
import jakarta.ws.rs.core.UriInfo;
10-
11-
import java.net.URI;
12-
1319
import org.eclipse.microprofile.config.inject.ConfigProperty;
1420
import org.eclipse.microprofile.metrics.MetricUnits;
1521
import org.eclipse.microprofile.metrics.annotation.Counted;
1622
import org.eclipse.microprofile.metrics.annotation.Timed;
1723
import org.jboss.logging.Logger;
1824

19-
import com.google.api.client.googleapis.auth.oauth2.GoogleIdToken;
20-
import com.google.api.client.googleapis.auth.oauth2.GoogleIdToken.Payload;
21-
22-
import de.remsfal.core.api.AuthenticationEndpoint;
23-
import de.remsfal.core.model.UserModel;
24-
import de.remsfal.service.boundary.authentication.GoogleAuthenticator;
25-
import de.remsfal.service.boundary.authentication.SessionInfo;
26-
import de.remsfal.service.boundary.authentication.SessionManager;
27-
import de.remsfal.service.boundary.exception.UnauthorizedException;
28-
import de.remsfal.service.control.UserController;
25+
import java.net.URI;
2926

3027
/**
3128
* @author Alexander Stanik [alexander.stanik@htw-berlin.de]
@@ -38,7 +35,7 @@ public class AuthenticationResource implements AuthenticationEndpoint {
3835

3936
@Context
4037
UriInfo uri;
41-
38+
4239
@Context
4340
HttpHeaders headers;
4441

@@ -53,14 +50,15 @@ public class AuthenticationResource implements AuthenticationEndpoint {
5350

5451
@Inject
5552
Logger logger;
53+
@Inject
54+
HttpHeaders httpHeaders;
5655

5756

5857
@Override
59-
@Timed(name = "checksTimerLogin",unit = MetricUnits.MILLISECONDS)
58+
@Timed(name = "checksTimerLogin", unit = MetricUnits.MILLISECONDS)
6059
@Counted(name = "countedLogin")
6160
public Response login(final String route) {
62-
final String redirectUri = getAbsoluteUri()
63-
.toASCIIString().replace("/login", "/session");
61+
final String redirectUri = getAbsoluteUri().toASCIIString().replace("/login", "/session");
6462
final URI redirectUrl = authenticator.getAuthorizationCodeURI(redirectUri, route);
6563
return redirect(redirectUrl).build();
6664
}
@@ -83,33 +81,26 @@ public Response session(final String code, final String state, final String erro
8381
}
8482

8583
private Response createSession(final UserModel user, final String route) {
86-
final URI redirectUri = getAbsoluteUriBuilder()
87-
.replacePath(route)
88-
.build();
89-
final SessionInfo sessionInfo = sessionManager.sessionInfoBuilder()
90-
.userId(user.getId())
91-
.userEmail(user.getEmail())
92-
.build();
93-
return redirect(redirectUri)
94-
.cookie(sessionManager.encryptSessionCookie(sessionInfo))
95-
.build();
84+
final URI redirectUri = getAbsoluteUriBuilder().replacePath(route).build();
85+
final NewCookie accessToken = sessionManager.generateAccessToken(
86+
sessionManager.sessionInfoBuilder(SessionManager.ACCESS_COOKIE_NAME).userId(user.getId())
87+
.userEmail(user.getEmail()).build());
88+
final NewCookie refreshToken = sessionManager.generateRefreshToken(user.getId(), user.getEmail());
89+
return redirect(redirectUri).cookie(accessToken, refreshToken).build();
9690
}
9791

98-
@Timed(name = "checksTimerLogout",unit = MetricUnits.MILLISECONDS)
92+
@Timed(name = "checksTimerLogout", unit = MetricUnits.MILLISECONDS)
9993
@Counted(name = "countedLogout")
10094
@Override
10195
public Response logout() {
102-
final URI redirectUri = getAbsoluteUriBuilder()
103-
.replacePath("/")
104-
.build();
105-
return redirect(redirectUri)
106-
.cookie(sessionManager.removalSessionCookie())
107-
.build();
96+
final URI redirectUri = getAbsoluteUriBuilder().replacePath("/").build();
97+
sessionManager.logout(httpHeaders.getCookies());
98+
return redirect(redirectUri).cookie(sessionManager.removalCookie(SessionManager.ACCESS_COOKIE_NAME),
99+
sessionManager.removalCookie(SessionManager.REFRESH_COOKIE_NAME)).build();
108100
}
109101

110102
private Response.ResponseBuilder redirect(final URI redirectUrl) {
111-
return Response.status(302)
112-
.header("location", redirectUrl);
103+
return Response.status(302).header("location", redirectUrl);
113104
}
114105

115106
private URI getAbsoluteUri() {
@@ -118,19 +109,19 @@ private URI getAbsoluteUri() {
118109

119110
private UriBuilder getAbsoluteUriBuilder() {
120111
final String forwardedHostHeader = headers.getHeaderString("X-Forwarded-Host");
121-
if(enableForwardedHost && forwardedHostHeader != null) {
112+
if (enableForwardedHost && forwardedHostHeader != null) {
122113
logger.infov("Proxy is enabled. X-Forwarded-Host: {0}", forwardedHostHeader);
123114
final UriBuilder builder = uri.getAbsolutePathBuilder();
124115
final String[] parts = forwardedHostHeader.split(":");
125-
if(parts.length > 0) {
116+
if (parts.length > 0) {
126117
logger.debugv("Host: {0}", parts[0]);
127118
builder.host(parts[0]);
128119
}
129-
if(parts.length > 1) {
120+
if (parts.length > 1) {
130121
try {
131122
logger.debugv("Port: {0}", parts[1]);
132123
builder.port(Integer.parseUnsignedInt(parts[1]));
133-
} catch(NumberFormatException e) {
124+
} catch (NumberFormatException e) {
134125
logger.errorv("Invalid port in X-Forwarded-Host header {0}", parts[1], e);
135126
}
136127
}
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
package de.remsfal.service.boundary.authentication;
22

3+
import de.remsfal.core.api.AuthenticationEndpoint;
4+
import de.remsfal.service.boundary.exception.TokenExpiredException;
35
import jakarta.annotation.Priority;
46
import jakarta.inject.Inject;
57
import jakarta.ws.rs.Priorities;
@@ -13,7 +15,7 @@
1315

1416
@Provider
1517
@Priority(Priorities.HEADER_DECORATOR + 1)
16-
public class HeaderExtensionResponseFilter implements ContainerResponseFilter {
18+
public class HeaderExtensionResponseFilter implements ContainerResponseFilter {
1719

1820
@Inject
1921
SessionManager sessionManager;
@@ -22,22 +24,43 @@ public class HeaderExtensionResponseFilter implements ContainerResponseFilter {
2224
Logger logger;
2325

2426
@Override
25-
public void filter(ContainerRequestContext requestContext,
26-
ContainerResponseContext responseContext) {
27-
try{
28-
final Cookie sessionCookie = sessionManager.findSessionCookie(requestContext.getCookies());
29-
if (sessionCookie != null) {
30-
SessionInfo sessionInfo = sessionManager.decryptSessionCookie(sessionCookie);
31-
32-
if (sessionInfo != null && sessionInfo.isValid()) {
33-
if (sessionInfo.shouldRenew()) {
34-
Cookie newSessionCookie = sessionManager.renewSessionCookie(sessionInfo);
35-
responseContext.getHeaders().add("Set-Cookie", newSessionCookie.getValue());
36-
}
27+
public void filter(ContainerRequestContext requestContext, ContainerResponseContext responseContext) {
28+
29+
if (AuthenticationEndpoint.isAuthenticationPath(requestContext.getUriInfo().getPath())) {
30+
logger.infov("Skipping HeaderExtensionResponseFilter for authentication path: {0}",
31+
requestContext.getUriInfo().getPath());
32+
return;
33+
}
34+
try {
35+
Cookie accessToken = sessionManager.findAccessTokenCookie(requestContext.getCookies());
36+
if (accessToken == null) {
37+
Cookie refreshToken = sessionManager.findRefreshTokenCookie(requestContext.getCookies());
38+
if (refreshToken != null) {
39+
renewTokens(requestContext, responseContext);
40+
}
41+
}
42+
if (accessToken != null) {
43+
try {
44+
sessionManager.decryptAccessTokenCookie(accessToken);
45+
} catch (TokenExpiredException e) {
46+
logger.info("Access token expired: " + e.getMessage());
47+
renewTokens(requestContext, responseContext);
3748
}
3849
}
3950
} catch (Exception e) {
4051
logger.error("Error in HeaderExtensionResponseFilter: " + e.getMessage());
4152
}
4253
}
54+
55+
private void renewTokens(ContainerRequestContext requestContext, ContainerResponseContext responseContext) {
56+
try {
57+
SessionManager.TokenRenewalResponse response = sessionManager.renewTokens(requestContext.getCookies());
58+
responseContext.getHeaders().add("Set-Cookie", response.getAccessToken());
59+
responseContext.getHeaders().add("Set-Cookie", response.getRefreshToken());
60+
} catch (Exception e) {
61+
logger.error("Error renewing tokens: " + e.getMessage());
62+
}
63+
}
64+
65+
4366
}

0 commit comments

Comments
 (0)