-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Story: [CCLS 2191] Authentication Starter (#4)
* add auth starter * publish library * return JSON response, add logging * log specific failure reason * clean up logging and documentation * fix log * add access denied handler * improved logging * add mermaid graph * update mermaid graph for clarity * address review comments * add openapi security scheme configuration, cleanup
- Loading branch information
Showing
24 changed files
with
1,091 additions
and
16 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
170 changes: 170 additions & 0 deletions
170
laa-ccms-spring-boot-starters/laa-ccms-spring-boot-starter-auth/README.md
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
|
||
``` | ||
|
||
|
39 changes: 39 additions & 0 deletions
39
laa-ccms-spring-boot-starters/laa-ccms-spring-boot-starter-auth/build.gradle
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() | ||
} |
64 changes: 64 additions & 0 deletions
64
...ot-starter-auth/src/main/java/uk/gov/laa/ccms/springboot/auth/ApiAccessDeniedHandler.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 { | ||
|
||
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); | ||
} | ||
|
||
} |
Oops, something went wrong.