Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Story: [CCLS 2191] Authentication Starter #4

Merged
merged 12 commits into from
Jun 7, 2024
5 changes: 5 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,11 @@ subprojects {
it.jvmArgs = ["--add-opens=java.base/java.lang=ALL-UNNAMED"]
}

repositories {
mavenCentral()
gradlePluginPortal()
}

apply plugin: 'maven-publish'

publishing.repositories {
Expand Down
3 changes: 1 addition & 2 deletions buildSrc/src/main/groovy/gradle-plugin-conventions.gradle
Original file line number Diff line number Diff line change
@@ -1,14 +1,13 @@
plugins {
id 'groovy'
id 'java-gradle-plugin'
}

java {
toolchain.languageVersion.set(JavaLanguageVersion.of(javaVersion))
}

dependencies {
implementation gradleApi()
implementation localGroovy()
testImplementation 'org.junit.jupiter:junit-jupiter-api:5.10.2'
testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine'
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ plugins {
id 'java-library'
}

group = 'uk.gov.laa.ccms.springboot'

java {
toolchain.languageVersion.set(JavaLanguageVersion.of(javaVersion))
}
Expand Down
5 changes: 0 additions & 5 deletions laa-ccms-java-gradle-plugin/build.gradle
Original file line number Diff line number Diff line change
@@ -1,15 +1,10 @@
plugins {
id 'gradle-plugin-conventions'
id 'java-gradle-plugin'
id 'maven-publish'
}

group = 'uk.gov.laa.ccms.java'

repositories {
gradlePluginPortal()
}

dependencies {
implementation "com.github.ben-manes:gradle-versions-plugin:${gradleVersionsPluginVersion}"
}
Expand Down
10 changes: 2 additions & 8 deletions laa-ccms-spring-boot-gradle-plugin/build.gradle
Original file line number Diff line number Diff line change
@@ -1,23 +1,17 @@
plugins {
id 'java-gradle-plugin'
id 'maven-publish'
id 'java'
id 'groovy'
id 'java-gradle-plugin'
id 'maven-publish'
}

group = 'uk.gov.laa.ccms.springboot'

repositories {
gradlePluginPortal()
}

java {
toolchain.languageVersion.set(JavaLanguageVersion.of(javaVersion))
}

dependencies {
implementation gradleApi()
implementation localGroovy()

// Make sure we're using the same version of the Java plugin that we're adding into the starters
implementation project(':laa-ccms-java-gradle-plugin')
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
# LAA CCMS SpringBoot Authentication Starter

This starter will enable authentication on endpoints you have specified in your application configuration.
Roles can be defined to categorise groups of endpoints under different levels of access. These roles can then be assigned
to clients.

## Usage

### Declare the dependency

To enable this in your application, declare the following:

```groovy
dependencies {
implementation 'uk.gov.laa.ccms.springboot:laa-ccms-spring-boot-starter-auth'
}
```

### Configure via application properties

Here you will need to define several properties to ensure authentication behaves as expected:

- `authentication-header` - The name of the HTTP header used to send and receive the API access token.
- `authorized-clients` - The list of clients who are authorized to access the API, and their roles. This is a JSON formatted string, with the top level being a list and each contained item representing a client's credentials, containing the name of the client, the roles it has access to and the access token associated with it.
- `authorized-roles` - The list of roles that can be used to access the API, and the URIs they enable access to. This is a JSON formatted string, with the top level being a list and each contained item representing an authorized role, containing the name of the role and the URIs that it enables access to.
- `unprotected-uris` - The list of URIs which do not require any authentication. These may be relating to API documentation, static resources or any other content which is not sensitive.

Access tokens should be generated as a `UUID4` string.

```yaml
laa.ccms.springboot.starter.auth:
authentication-header: "Authorization"
authorized-clients: '[
{
"name": "client1",
"roles": [
"GROUP1"
],
"token": "b7bbdb3d-d0b9-4632-b752-b2e0f9486baf"
},
{
"name": "client2",
"roles": [
"GROUP2"
],
"token": "1fd84ad9-760d-401f-8cf0-7a80aa42566c"
},
{
"name": "client3",
"roles": [
"GROUP1",
"GROUP2"
],
"token": "5d925478-a8a2-4b76-863a-3fb87dcbcb95"
}
]'
authorized-roles: '[
{
"name": "GROUP1",
"URIs": [
"/resource1/requires-group1-role/**"
]
},
{
"name": "GROUP2",
"URIs": [
"/*/requires-group2-role/**"
]
}
]'
unprotected-uris: [ "/actuator/**", "/resource1/unrestricted/**" ]
```

## Behaviour

Authentication of endpoints will behave as follows.

### Unprotected URIs

Unprotected URIs will not require any authentication. Authentication headers will be ignored.

### Protected URIs

If a client attempts to access a protected URI, they will receive one of 3 responses depending on the scenario:

- Invalid or no access token present / wrong header used: 401 Unauthorized
- Valid access token present, client's role **does not** permit access to the requested URI or the URI does not exist: 403 Forbidden
- Valid access token present, client's role **does** permit access to the requested URI: 2XX (Success) / normal response

```mermaid
graph

subgraph key["Key"]
green["Spring Security"]
blue["Auth Starter"]
end

client["Client"]

subgraph api["API"]

subgraph filterChain["Filter Chain"]
authenticationFilter["API Authentication Filter"]
authorizationFilter["Authorization Filter"]
end

authenticationService["API Authentication Service"]

authorizationM["Authorization Manager"]
rmdAuthorizationM["RequestMatcherDelegatingAuthorizationManager"]
authorityAuthorizationM["Authority Authorization Manager"]

authorizationCheck{"Client<br>Authorized?"}
accessDeniedHandler["Access Denied Handler"]
businessLogic["Business Logic"]

subgraph securityContext["Security Context"]
creds["Credentials"]
end

authenticationCheck{"Client<br>Authenticated?"}

end

client -- <span style='color:black;font-weight:bold;font-size:25px' style=''>START</span><br>1. Request (protected endpoint) --> authenticationFilter

authenticationFilter -- 2. Create authentication token --> authenticationService

authenticationFilter -- 3. check authentication --> authenticationCheck

authenticationCheck -- 4a. Yes - Store authentication token --> creds

authenticationCheck -- 4b. No - 401 Unauthorized --> client

authenticationFilter -- 5. doFilter --> authorizationFilter

authorizationFilter -- 6. Get authentication token --> creds

authorizationFilter -- 7. Check authorization --> authorizationM

authorizationM --> rmdAuthorizationM

rmdAuthorizationM -- 8. Identify matching request mapping--> rmdAuthorizationM

rmdAuthorizationM --> authorityAuthorizationM

authorityAuthorizationM -- 9. Compare client's role<br>against role required<br>for endpoint --> authorityAuthorizationM

authorityAuthorizationM --> authorizationCheck

authorizationCheck -- 10a. No --> accessDeniedHandler
accessDeniedHandler -- 11a. 403 Forbidden --> client

authorizationCheck -- 10b. Yes --> businessLogic
businessLogic -- 11b. Normal response --> client


classDef green fill:#206020,stroke:#333,stroke-width:2px;
classDef blue fill:#002db3,stroke:#333,stroke-width:2px;
class green,authorizationFilter,authM,authP,providerM,securityContext,creds,authorizationM,rmdAuthorizationM,authorizationCheck,authorityAuthorizationM green
class blue,authenticationFilter,authenticationService,accessDeniedHandler,authenticationCheck blue
linkStyle 0 stroke-width:3px,stroke:black,color:black

linkStyle 4 stroke:red,color:red
linkStyle 14 stroke:red,color:red
linkStyle 16 stroke:green,color:green

```


Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
plugins {
id 'spring-boot-starter-conventions'
}

dependencies {

compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'

implementation(project(':laa-ccms-java-gradle-plugin')) {
transitive = false
}

implementation 'io.swagger.core.v3:swagger-models:2.2.22'

implementation 'org.springframework.boot:spring-boot-starter-web'

implementation 'jakarta.servlet:jakarta.servlet-api'

implementation 'jakarta.ws.rs:jakarta.ws.rs-api'

implementation 'com.fasterxml.jackson.jakarta.rs:jackson-jakarta-rs-json-provider'

implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'org.springframework.boot:spring-boot-starter-validation'

testImplementation 'org.assertj:assertj-core:3.4.1'
}

publishing.publications {
library(MavenPublication) {
from components.java
}
}


test {
useJUnitPlatform()
}
farrell-m marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
package uk.gov.laa.ccms.springboot.auth;

import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.ws.rs.core.Response;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.MediaType;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.stereotype.Component;

import java.io.IOException;

/**
* Exception Handler for requests that have been authenticated, but do not have sufficient privileges to access
* the requested endpoint.
*/
@Slf4j
@Component
public class ApiAccessDeniedHandler implements AccessDeniedHandler {
PhilDigitalJustice marked this conversation as resolved.
Show resolved Hide resolved

ObjectMapper objectMapper;

/**
* Creates an instance of the handler, with an object mapper to write the request body.
*
* @param objectMapper for writing the request body.
*/
@Autowired
ApiAccessDeniedHandler(ObjectMapper objectMapper) {
this.objectMapper = objectMapper;
}

/**
* Constructs the response object to return to the client, with a 403 Forbidden status and matching
* response body using the {@link ErrorResponse} model.
*
* @param request that resulted in an <code>AccessDeniedException</code>
* @param response so that the client can be advised of the failure
* @param accessDeniedException that caused the invocation
* @throws IOException -
* @throws ServletException -
*/
@Override
public void handle(HttpServletRequest request, HttpServletResponse response,
AccessDeniedException accessDeniedException) throws IOException, ServletException {
int code = HttpServletResponse.SC_FORBIDDEN;
response.setStatus(code);
response.setContentType(MediaType.APPLICATION_JSON_VALUE);

String status = Response.Status.FORBIDDEN.getReasonPhrase();
String message = accessDeniedException.getMessage();

ErrorResponse errorResponse = new ErrorResponse(code, status, message);

response.getWriter().write(objectMapper.writeValueAsString(errorResponse));

log.info("Request rejected for endpoint '{} {}': {}", request.getMethod(), request.getRequestURI(), message);
}

}
Loading
Loading