Backend For Frontend BFF
Repository
https://github.com/Mehdi-HAFID/bff
Credit
All credit for the BFF and the Reverse Proxy implemented in Nidam goes to Jérôme Wacongne and his article in baeldung.com OAuth2 Backend for Frontend With Spring Cloud Gateway. The BFF and the Reverse Proxy in Nidam is pretty much a copy paste from his article. I strongly recommend to go there and read his article.
Objective
The BFF has mainly 3 responsibilities:
- Login flow initiation: when the user clicks the
Login
button in the frontend, the whole OAuth 2Authorization Code
Flow is started and the login form is displayed. - Token Storage: Tokens issued by the authorization server are saved and a corresponding
SESSION
Cookie is set which is what the frontend uses. - SESSION Cookie Translation: All calls by the frontend are sent to the BFF, with the SESSION Cookie. Before The BFF forwards the request to the resource server, it attaches the Token associated with the Cookie.
Crucial Dependency
Jérôme Wacongne has used a dependency called spring-addons-starter-oidc which he himself created. This version of Nidam will ship with this dependency. A future version will aim to remove dependency and only use Spring Security code. But for now, we will use this dependency. The dependency aims to simplify security configuration code.
pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.2.0</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>nidam</groupId>
<artifactId>bff</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>bff</name>
<description>bff</description>
<properties>
<java.version>17</java.version>
<spring-cloud.version>2023.0.0</spring-cloud.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<dependency>
<groupId>com.c4-soft.springaddons</groupId>
<artifactId>spring-addons-starter-oidc</artifactId>
<version>7.5.3</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.projectreactor</groupId>
<artifactId>reactor-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
The BFF is an OAuth 2 Client, and it uses Spring Gateway for redirection.
Configuration
Declaration of URLs and client id and secret
scheme: http
hostname: localhost
reverse-proxy-port: 7080
reverse-proxy-uri: ${scheme}://${hostname}:${reverse-proxy-port}
authorization-server-prefix: /auth
issuer: ${reverse-proxy-uri}${authorization-server-prefix}
client-id: client
client-secret: secret
username-claim-json-path: $.sub
authorities-json-path: $.authorities
bff-port: 7081
bff-prefix: /bff
resource-server-port: 4003
audience:
server:
port: ${bff-port}
The BFF Core Functionality
The following is the most important piece of code in the BFF. It takes all calls made to /api/**
and redirects them to the resource server.
- First, the front calls
http://localhost:7080/bff/api/something
which is handled by the reverse proxy and forwards it tohttp://localhost:7081/api/something
. - The BFF removes the
/api
and using Spring GatewayTokenRelay=
attaches the corresponding token before forwarding the request to the resource serverhttp://localhost:4003/something
.
The SaveSession
filter persists the token that is generated and returned by the authorization server during the login process.
spring:
cloud:
gateway:
routes:
- id: bff
uri: ${scheme}://${hostname}:${resource-server-port}
predicates:
- Path=/api/**
filters:
- DedupeResponseHeader=Access-Control-Allow-Credentials Access-Control-Allow-Origin
- TokenRelay=
- SaveSession
- StripPrefix=1
OAuth 2 Client Definition
The BFF is an OAuth 2 Client, so we need to declare the properties we defined in the authorization server.
spring:
security:
oauth2:
client:
provider:
token-generator:
issuer-uri: ${issuer}
registration:
token-generator:
provider: token-generator
authorization-grant-type: authorization_code
client-id: ${client-id}
client-secret: ${client-secret}
scope: openid
spring-addons-starter-oidc Dependency
I will not explain it better than Jérôme Wacongne so, I will just quote his explanation for this part of the code.
Let’s understand the three main sections:
ops
, with provider(s) specific values. This enables us to specify the JSON path of the claims to convert to Spring authorities (with optional prefixes and case transformation for each). Ifaud
property is not empty, spring-addons adds an audience validator to the JWT decoders.
client
, when security-matchers are not empty, this section triggers the creation of aSecurityFilterChain
bean withoauth2Login()
. Here, withclient-uri
property, we force the usage of the reverse-proxy URI as a base for all redirections (instead of the BFF internal URI). Also, as we are using SPAs, we ask the BFF to expose the CSRF token in a cookie accessible to Javascript. Last, to prevent CORS errors, we ask that the BFF respond to the RP-Initiated Logout with201 status
(instead of3xx
), which gives SPAs the ability to intercept this response and ask the browser to process it in a request with a new origin.
resourceserver
, this requests for a secondSecurityFilterChain
bean withoauth2ResourceServer()
. This filter chain having an@Order
with the lowest precedence will process all of the requests that weren’t matched by the security matchers from the clientSecurityFilterChain
. We use it for all resources for which a session is not desirable: endpoints that aren’t involved in login or routing withTokenRelay
.
com:
c4-soft:
springaddons:
oidc:
# Trusted OpenID Providers configuration (with authorities mapping)
ops:
- iss: ${issuer}
authorities:
- path: ${authorities-json-path}
aud: ${audience}
# SecurityFilterChain with oauth2Login() (sessions and CSRF protection enabled)
client:
client-uri: ${reverse-proxy-uri}${bff-prefix}
security-matchers:
- /api/**
- /login/**
- /oauth2/**
- /logout
permit-all:
- /api/**
- /login/**
- /oauth2/**
csrf: cookie-accessible-from-js
oauth2-redirections:
rp-initiated-logout: ACCEPTED
# SecurityFilterChain with oauth2ResourceServer() (sessions and CSRF protection disabled)
resourceserver:
permit-all:
- /error
- /actuator/health/readiness
- /actuator/health/liveness