Skip to content

Commit 60bbba4

Browse files
Vinay-kalpaguriVinay Kumar Gupta Kalpaguri (EXT)
and
Vinay Kumar Gupta Kalpaguri (EXT)
authored
Made required code changes to adapt Entra Id for MFA (#325)
* Made required code changes to adapt Entra Id for MFA * Updated documentation for configuration file * updated artifact version to 3.0.9 --------- Co-authored-by: Vinay Kumar Gupta Kalpaguri (EXT) <[email protected]>
1 parent 3675914 commit 60bbba4

File tree

11 files changed

+312
-17
lines changed

11 files changed

+312
-17
lines changed

.github/workflows/main.yml

+3-3
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ jobs:
2020
if: github.repository == 'eiffel-community/eiffel-intelligence-frontend'
2121
# The type of runner that the job will run on
2222

23-
runs-on: ubuntu-latest
23+
runs-on: ubuntu-22.04
2424
env:
2525
EI_BACKEND_PORT: 8099
2626
M2_HOME: /opt/apache-maven-3.6.3
@@ -54,7 +54,7 @@ jobs:
5454
if: github.repository == 'eiffel-community/eiffel-intelligence-frontend'
5555
# The type of runner that the job will run on
5656

57-
runs-on: ubuntu-latest
57+
runs-on: ubuntu-22.04
5858
env:
5959
EI_BACKEND_PORT: 8099
6060
M2_HOME: /opt/apache-maven-3.6.3
@@ -113,7 +113,7 @@ jobs:
113113
if: github.repository == 'eiffel-community/eiffel-intelligence-frontend' && github.event_name == 'push'
114114
# The type of runner that the job will run on
115115

116-
runs-on: ubuntu-latest
116+
runs-on: ubuntu-22.04
117117
env:
118118
EI_BACKEND_PORT: 8099
119119
M2_HOME: /opt/apache-maven-3.6.3

pom.xml

+19-1
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
<modelVersion>4.0.0</modelVersion>
77
<groupId>com.github.ericsson</groupId>
88
<artifactId>eiffel-intelligence-frontend</artifactId>
9-
<version>3.0.8</version>
9+
<version>3.0.9</version>
1010
<packaging>war</packaging>
1111

1212
<parent>
@@ -108,6 +108,24 @@
108108
</exclusion>
109109
</exclusions>
110110
</dependency>
111+
<dependency>
112+
<groupId>com.azure.spring</groupId>
113+
<artifactId>spring-cloud-azure-starter-active-directory</artifactId>
114+
<version>4.5.0</version>
115+
</dependency>
116+
<dependency>
117+
<groupId>org.springframework.boot</groupId>
118+
<artifactId>spring-boot-starter-oauth2-client</artifactId>
119+
</dependency>
120+
<dependency>
121+
<groupId>org.springframework.boot</groupId>
122+
<artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
123+
</dependency>
124+
<dependency>
125+
<groupId>com.github.ulisesbocchio</groupId>
126+
<artifactId>jasypt-spring-boot-starter</artifactId>
127+
<version>2.1.2</version>
128+
</dependency>
111129

112130
<dependency>
113131
<groupId>org.apache.commons</groupId>

src/main/java/com/ericsson/ei/frontend/EIRequestsController.java

+10
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
import org.slf4j.Logger;
3939
import org.slf4j.LoggerFactory;
4040
import org.springframework.beans.factory.annotation.Autowired;
41+
import org.springframework.beans.factory.annotation.Value;
4142
import org.springframework.http.HttpHeaders;
4243
import org.springframework.http.HttpStatus;
4344
import org.springframework.http.MediaType;
@@ -50,16 +51,22 @@
5051

5152
import com.ericsson.ei.frontend.exceptions.EiBackendInstancesException;
5253
import com.ericsson.ei.frontend.utils.EIRequestsControllerUtils;
54+
import com.ericsson.ei.frontend.utils.EntraIdTokenIssuer;
5355

5456
@RestController
5557
public class EIRequestsController {
5658

5759
private static final Logger LOG = LoggerFactory.getLogger(EIRequestsController.class);
5860
private static final String X_AUTH_TOKEN = "x-auth-token";
61+
private static final String AZURE_TOKEN = "azure-token";
5962
List<String> HEADERS_TO_COPY = new ArrayList<>(Arrays.asList(X_AUTH_TOKEN, "authorization"));
6063

6164
@Autowired
6265
private EIRequestsControllerUtils eiRequestsControllerUtils;
66+
@Autowired
67+
private EntraIdTokenIssuer entraIdTokenIssuer;
68+
@Value("${spring.cloud.azure.active-directory.enabled}")
69+
private Boolean azureEnabled;
6370

6471
/**
6572
* Bridge all Eiffel Intelligence HTTP GET requests.
@@ -280,6 +287,9 @@ private HttpHeaders getHeadersFromResponse(HttpHeaders headers, CloseableHttpRes
280287
if (headerShouldBeCopied) {
281288
headerNameList.add(header.getName());
282289
headers.add(header.getName(), header.getValue());
290+
if (azureEnabled) {
291+
headers.add(AZURE_TOKEN, entraIdTokenIssuer.getAccessToken());
292+
}
283293
} else {
284294
notCopiedHeaderNameList.add(header.getName());
285295
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
package com.ericsson.ei.frontend.config;
2+
3+
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
4+
import org.springframework.context.annotation.Configuration;
5+
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
6+
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
7+
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
8+
9+
@SuppressWarnings("deprecation")
10+
@Configuration
11+
@EnableWebSecurity
12+
@ConditionalOnProperty(name = "spring.cloud.azure.active-directory.enabled", havingValue = "false")
13+
public class SecurityConfig extends WebSecurityConfigurerAdapter {
14+
15+
@Override
16+
protected void configure(HttpSecurity http) throws Exception {
17+
http
18+
.csrf().disable() // Disable CSRF protection
19+
.authorizeRequests()
20+
.anyRequest().permitAll()
21+
.and()
22+
.formLogin().disable();
23+
}
24+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
package com.ericsson.ei.frontend.config;
2+
3+
import org.springframework.beans.factory.annotation.Value;
4+
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
5+
import org.springframework.context.annotation.Bean;
6+
import org.springframework.context.annotation.Configuration;
7+
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
8+
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
9+
import org.springframework.security.config.http.SessionCreationPolicy;
10+
import org.springframework.security.oauth2.core.DelegatingOAuth2TokenValidator;
11+
import org.springframework.security.oauth2.core.OAuth2Error;
12+
import org.springframework.security.oauth2.core.OAuth2TokenValidator;
13+
import org.springframework.security.oauth2.core.OAuth2TokenValidatorResult;
14+
import org.springframework.security.oauth2.jwt.Jwt;
15+
import org.springframework.security.oauth2.jwt.JwtDecoder;
16+
import org.springframework.security.oauth2.jwt.NimbusJwtDecoder;
17+
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationConverter;
18+
19+
@SuppressWarnings("deprecation")
20+
@Configuration
21+
@ConditionalOnProperty(name = "spring.cloud.azure.active-directory.enabled", havingValue = "true")
22+
public class SecurityConfigAzureAD extends WebSecurityConfigurerAdapter {
23+
24+
@Value("${spring.cloud.azure.active-directory.credential.client-id}")
25+
private String clientId;
26+
27+
@Value("${spring.cloud.azure.active-directory.profile.tenant-id}")
28+
private String tenantId;
29+
30+
@Override
31+
protected void configure(HttpSecurity http) throws Exception {
32+
http
33+
.csrf().disable()
34+
.authorizeRequests()
35+
.antMatchers("/authentication/*","/status","/actuator/**").permitAll()
36+
.anyRequest().authenticated()
37+
.and()
38+
.oauth2Login() // Enable Azure MFA for H2M
39+
.and()
40+
.sessionManagement()
41+
.sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED)
42+
.and()
43+
.oauth2ResourceServer()
44+
.jwt()
45+
.jwtAuthenticationConverter(jwtAuthenticationConverter()) // Configure JWT converter
46+
.decoder(jwtDecoder()); // Configure JWT decoder to handle client credentials flow
47+
}
48+
49+
@Bean
50+
public JwtAuthenticationConverter jwtAuthenticationConverter() {
51+
JwtAuthenticationConverter converter = new JwtAuthenticationConverter();
52+
// You can add additional configuration for authorities mapping here
53+
return converter;
54+
}
55+
56+
@Bean
57+
public JwtDecoder jwtDecoder() {
58+
// Use Azure AD's JWKS endpoint
59+
String jwkSetUri = "https://login.microsoftonline.com/" + tenantId + "/discovery/v2.0/keys";
60+
NimbusJwtDecoder jwtDecoder = NimbusJwtDecoder.withJwkSetUri(jwkSetUri).build();
61+
62+
// token validators (e.g., audience, issuer) to validate the token properly
63+
OAuth2TokenValidator<Jwt> audienceValidator = new OAuth2TokenValidator<Jwt>() {
64+
@Override
65+
public OAuth2TokenValidatorResult validate(Jwt token) {
66+
if (!token.getAudience().contains("api://" + clientId)) {
67+
return OAuth2TokenValidatorResult.failure(new OAuth2Error("invalid_audience", "Invalid audience", null));
68+
}
69+
return OAuth2TokenValidatorResult.success();
70+
}
71+
};
72+
73+
OAuth2TokenValidator<Jwt> issuerValidator = new OAuth2TokenValidator<Jwt>() {
74+
@Override
75+
public OAuth2TokenValidatorResult validate(Jwt token) {
76+
String issuer = token.getIssuer().toString();
77+
// Validate the issuer for both v1.0 and v2.0 endpoints
78+
if (!issuer.equals("https://login.microsoftonline.com/" + tenantId +"/v2.0") && !issuer.equals("https://sts.windows.net/" + tenantId + "/")) {
79+
return OAuth2TokenValidatorResult.failure(new OAuth2Error("invalid_issuer", "Invalid issuer", null));
80+
}
81+
return OAuth2TokenValidatorResult.success();
82+
}
83+
};
84+
85+
// Chain validators together (audience and issuer)
86+
OAuth2TokenValidator<Jwt> tokenValidator = new DelegatingOAuth2TokenValidator<>(audienceValidator, issuerValidator);
87+
jwtDecoder.setJwtValidator(tokenValidator); // Apply validators to the decoder
88+
return jwtDecoder;
89+
}
90+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
package com.ericsson.ei.frontend.utils;
2+
3+
import org.springframework.beans.factory.annotation.Value;
4+
import org.springframework.stereotype.Component;
5+
6+
import com.azure.core.credential.TokenCredential;
7+
import com.azure.core.credential.TokenRequestContext;
8+
import com.azure.identity.ClientSecretCredentialBuilder;
9+
10+
@Component
11+
public class EntraIdTokenIssuer {
12+
13+
@Value("${spring.cloud.azure.active-directory.credential.client-id}")
14+
private String clientId;
15+
16+
@Value("${spring.cloud.azure.active-directory.credential.client-secret}")
17+
private String clientSecret;
18+
19+
@Value("${spring.cloud.azure.active-directory.profile.tenant-id}")
20+
private String tenantId;
21+
22+
@Value("${spring.cloud.azure.active-directory.api-scope}")
23+
private String scope;
24+
25+
/**
26+
* Retrieves an access token for the configured Azure AD scope.
27+
* @return the access token as a string
28+
* @throws RuntimeException if token retrieval fails
29+
*/
30+
public String getAccessToken() {
31+
// Build the ClientSecretCredential using Spring's property values
32+
TokenCredential clientSecretCredential = new ClientSecretCredentialBuilder()
33+
.clientId(clientId)
34+
.clientSecret(clientSecret)
35+
.tenantId(tenantId)
36+
.build();
37+
// Get the token using the scope defined (default is ".default")
38+
String token = clientSecretCredential
39+
.getToken(new TokenRequestContext().addScopes(scope))
40+
.block()
41+
.getToken();
42+
43+
// Return the token string
44+
return token;
45+
}
46+
}

src/main/resources/application.properties

+8
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,14 @@ ei.eiffel.documentation.urls={ "EI front-end on GitHub": "https://github.com/eif
2424
"Eiffel protocol on Github": "https://github.com/eiffel-community/eiffel",\
2525
"User guide for test rules page": "https://github.com/eiffel-community/eiffel-intelligence-frontend/blob/master/wiki/test-rules.md" }
2626

27+
# Microsoft Entra Id Config
28+
spring.cloud.azure.active-directory.credential.client-id=
29+
spring.cloud.azure.active-directory.credential.client-secret=
30+
spring.cloud.azure.active-directory.redirect-uri=
31+
spring.cloud.azure.active-directory.enabled=false
32+
spring.cloud.azure.active-directory.profile.tenant-id=
33+
spring.cloud.azure.active-directory.api-scope=
34+
2735
#### LOGGING #########
2836
logging.level.root: INFO
2937
logging.level.org.springframework.web: INFO

src/main/resources/templates/index.html

+6
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,12 @@
8787
</ul>
8888
</div>
8989
</li>
90+
<li class="nav-item">
91+
<!-- Logout button -->
92+
<a class="nav-link" href="/eifrontend/logout">
93+
<i class="fa fa-sign-out"></i>FrontendLogout
94+
</a>
95+
</li>
9096
</ul>
9197
</header>
9298

wiki/authentication.md

+70
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
# EI Frontend Authentication Documentation
2+
3+
## Overview
4+
5+
This documentation explains how to authenticate with the EI Frontend using different authentication mechanisms for human-based (H2M) and machine-to-machine (M2M) scenarios.
6+
7+
Authentication is not a requirement but can be turned on and off in the
8+
application properties file with the 'spring.cloud.azure.active-directory.enabled' property.
9+
10+
### Authentication Types:
11+
12+
- **H2M Authentication**: For human-based authentication, users will be redirected to the Microsoft login page for Single Sign-On (SSO). Multi-Factor Authentication (MFA) may be prompted if required.
13+
14+
- **M2M Authentication**: In M2M scenarios, the client application directly makes a `GET` request to the `/authentication/login` API. The client must provide a username and password using basic authentication. The response headers will includes two tokens:
15+
- `xauth-token`: Required for making requests to the `/subscriptions` API (POST, PUT, DELETE).
16+
- `azure-token`: Required for accessing all APIs when azure authentication is enabled.
17+
18+
**Note**: Include both xauth-token and azure-token in the headers for accessing `/subscriptions` API (POST, PUT, DELETE).
19+
20+
## Step 1: Authenticating to Obtain Tokens
21+
22+
To authenticate and receive the necessary tokens, make a `GET` request to the `/authentication/login` endpoint.
23+
24+
### Request Example:
25+
26+
curl -X GET -H "Content-Type: application/json" -u <user>:<password> http://localhost:8080/authentication/login
27+
28+
Example of full response
29+
30+
< HTTP/1.1 200
31+
< Vary: Origin
32+
< Vary: Access-Control-Request-Method
33+
< Vary: Access-Control-Request-Headers
34+
< Set-Cookie: JSESSIONID=4AC3653506A32762E220489C8230E37F; Path=/; HttpOnly
35+
< X-Auth-Token: 0c61a3d0-a154-42ac-ad08-aea6fda9de9a
36+
< azure-token: eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsIng1dCI6ImltaTBZMnowthlLeEJ0dEFxS19UdDVoWUJUayIsImtpZCI6ImltaTBZMnowZFlLeEJ0dEFxS19UdDVoWUJUayJ9.eyJhdWQiOiJhcGk6Ly83Mzg3N2ViNS1iNjUxLTRjYzAtYjI5Yi05NTYyZGRjMjgyYzciLCJpc3MiOiJodHRwczovL3N0cy53aW5kb3dzLm5ldC85MmU4NGNlYi1mYmZkLTQ3YWItYmU1Mi0wODBjNmI4Nzk1M2YvIiwiaWF0IjoxNzQwOTc2MzY5LCJuYmYiOjE3NDA5NzYzNjksImV4cCI6MTc0MDk4MDI2OSwiYWlvIjoiazJSZ1lEaktPNWR6cTU3aWh2bDgyMlkybU90cis1KzVFN0h6eTJ2aHU5WXRSVUhpb1Z3QSIsImFwcGlkIjoiNzM4NzdlYjUtYjY1MS00Y2MwLWIyOWItOTU2MmRkYzI4MmM3IiwiYXBwaWRhY3IiOiIxIiwiaWRwIjoiaHR0cHM6Ly9zdHMud2luZG93cy5uZXQvOTJlODRjZWItZmJmZC00N2FiLWJlNTItMDgwYzZiODc5NTNmLyIsIm9pZCI6IjBhMGRhODg0LTZhZmEtNGIwNC05NmIyLTVhYjJjODIxYjI0NCIsInJoIjoiMS5BUkVBNjB6b2t2MzdxMGUtVWdnTWE0ZVZQN1YtaDNOUnRzQk1zcHVWWXQzQ2dzY1JBQUFSQUEuIiwicm9sZXMiOlsiYXBpLnJlYWQiXSwic3ViIjoiMGEwZGE4ODQtNmFmYS00YjA0LTk2YjItNWFiMmM4MjFiMjQ0IiwidGlkIjoiOTJlODRjZWItZmJmZC00N2FiLWJlNTItMDgwYzZiODc5NTNmIiwidXRpIjoidURfa2htWHNJRWVEQUsyakFnd3hBQSIsInZlciI6IjEuMCJ9.IpUvwU7OA1x9B3OaE24QDRH0FXKfmo5dHJ1uygLb2sAsRzxHMOTbnerqKnggAqvDuFAQL4Cp3Cn6OINlA6Az8OoyxQwQVtXCb52fElik5N8HbjNhM6YVmC6lIRem0rm8W2SmGQad0gAhEgO_QdCDYQJP3_9EgFxuqjA2nyWNPzM7hZzniiAIm1eokRIb92Nb05mBirj9V2bs8viVnS--0c7P2nF6L8EfE2KeeHg675WsMuA9Gv8MOyxhMl5-H3Ri86m7Nx2G1rwMNQA3QwP-cOgjIDG5nNGvxjbDpycQSxl_qJS19qjpa3y6QhbcpnNZ3jf7_Se_LNyy-H4O6i2tmg
37+
< X-Content-Type-Options: nosniff
38+
< X-XSS-Protection: 1; mode=block
39+
< Cache-Control: no-cache, no-store, max-age=0, must-revalidate
40+
< Pragma: no-cache
41+
< Expires: 0
42+
< X-Frame-Options: DENY
43+
< Content-Type: application/json
44+
< Content-Length: 18
45+
< Date: Mon, 03 Mar 2025 04:37:49 GMT
46+
<
47+
* Connection #0 to host localhost left intact
48+
{"user":"myuser"}
49+
50+
## Step 2: Making API Requests with Tokens
51+
52+
Once you have obtained the tokens, include them in your API requests as follows:
53+
54+
### 1. Accessing All APIs (e.g., `/status`, `/backends`, `/templates`, etc.)
55+
56+
For all APIs include the `Authorization` header with the `azure-token`.
57+
58+
#### Request Example for GET
59+
60+
curl -X GET -H "Authorization: Bearer <azure-token>" http://localhost:8080/status
61+
62+
### 2. Accessing subscriptions APIs (e.g., `/subscriptions`)
63+
64+
curl -X POST -d @file_containing_list_of_json_objects -H "Content-Type: application/json" \
65+
-H "X-Auth-Token: <xauth-token>" -H "Authorization: Bearer <azure-token> \
66+
http://localhost:8080/subscriptions
67+
68+
**More information about how to make API Requests can be found [here](https://github.com/eiffel-community/eiffel-intelligence-frontend/blob/master/wiki/curl-examples.md).**
69+
70+

wiki/configuration.md

+20
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,26 @@ then these properties need to be set:
5353

5454
Read more in Spring documentation about how to enable HTTPS security in Spring applications.
5555

56+
## Microsoft Entra Id Config
57+
58+
Set the below properties to enable Microsoft EntraId SSO and Multi-factor authentication.By default it is set to false. Make it true and provide all the necessary data to enable this functionality.
59+
60+
spring.cloud.azure.active-directory.enabled=false
61+
spring.cloud.azure.active-directory.credential.client-id=<Your Client ID> # The client ID of your application registered in Microsoft Entra ID.
62+
spring.cloud.azure.active-directory.credential.client-secret=<Your Azure App Secret> # The client secret for your Azure application.
63+
spring.cloud.azure.active-directory.redirect-uri=<Redirect URI> # The redirect URI configured in your Azure application.
64+
spring.cloud.azure.active-directory.profile.tenant-id=<Your Tenant ID> # The tenant ID of your Microsoft Entra ID instance.
65+
spring.cloud.azure.active-directory.api-scope=<Application ID URI> # The Application ID URI of your Microsoft Entra ID instance.
66+
67+
### Example values for Entra Id
68+
69+
spring.cloud.azure.active-directory.enabled=true
70+
spring.cloud.azure.active-directory.credential.client-id=12345678-90ab-cdef-1234-567890abcdef
71+
spring.cloud.azure.active-directory.credential.client-secret=abcdef1234567890abcdef1234567890
72+
spring.cloud.azure.active-directory.redirect-uri=http://localhost:8080/login/oauth2/code/
73+
spring.cloud.azure.active-directory.profile.tenant-id=12345678-90ab-cdef-1234-567890abcdef
74+
spring.cloud.azure.active-directory.api-scope=api://12345678-90ab-cdef-1234-567890abcdef/.default
75+
5676
## Customize Documentation Links
5777
It is possible to add and change Documentation url links that is seen in the Eiffel
5878
Intelligence front-end Web-UI. Documentation url links is configured by

0 commit comments

Comments
 (0)