-
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.
- Loading branch information
Showing
18 changed files
with
712 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
72 changes: 72 additions & 0 deletions
72
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,72 @@ | ||
# 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 level 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/**" ] | ||
``` |
28 changes: 28 additions & 0 deletions
28
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,28 @@ | ||
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 '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' | ||
} | ||
|
||
test { | ||
useJUnitPlatform() | ||
} |
52 changes: 52 additions & 0 deletions
52
...t-starter-auth/src/main/java/uk/gov/laa/ccms/springboot/auth/ApiAuthenticationFilter.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,52 @@ | ||
package uk.gov.laa.ccms.springboot.auth; | ||
|
||
import jakarta.servlet.FilterChain; | ||
import jakarta.servlet.ServletRequest; | ||
import jakarta.servlet.ServletResponse; | ||
import jakarta.servlet.http.HttpServletRequest; | ||
import jakarta.servlet.http.HttpServletResponse; | ||
import org.springframework.beans.factory.annotation.Autowired; | ||
import org.springframework.http.MediaType; | ||
import org.springframework.security.core.Authentication; | ||
import org.springframework.security.core.context.SecurityContextHolder; | ||
import org.springframework.web.filter.GenericFilterBean; | ||
|
||
import java.io.IOException; | ||
|
||
/** | ||
* API access token authentication filter. | ||
*/ | ||
public class ApiAuthenticationFilter extends GenericFilterBean { | ||
|
||
ApiAuthenticationService authenticationService; | ||
|
||
@Autowired | ||
protected ApiAuthenticationFilter(ApiAuthenticationService authenticationService) { | ||
this.authenticationService = authenticationService; | ||
} | ||
|
||
/** | ||
* Filter reponsible for authenticating the client the made the request. Successful authentication results in the | ||
* authentication details being stored in the security context for further processing, and continuation of the | ||
* filter chain. Unsuccessful authentication results in a 401 UNAUTHORIZED response. | ||
* | ||
* @param request the http request object | ||
* @param response the http response object | ||
* @param filterChain the current filter chain | ||
* @throws IOException - | ||
*/ | ||
@Override | ||
public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain) | ||
throws IOException { | ||
try { | ||
Authentication authentication = authenticationService.getAuthentication((HttpServletRequest) request); | ||
SecurityContextHolder.getContext().setAuthentication(authentication); | ||
filterChain.doFilter(request, response); | ||
} catch (Exception ex) { | ||
HttpServletResponse httpResponse = (HttpServletResponse) response; | ||
httpResponse.setStatus(HttpServletResponse.SC_UNAUTHORIZED); | ||
httpResponse.setContentType(MediaType.APPLICATION_JSON_VALUE); | ||
httpResponse.getWriter().write(ex.getMessage()); | ||
} | ||
} | ||
} |
131 changes: 131 additions & 0 deletions
131
...-starter-auth/src/main/java/uk/gov/laa/ccms/springboot/auth/ApiAuthenticationService.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,131 @@ | ||
package uk.gov.laa.ccms.springboot.auth; | ||
|
||
import com.fasterxml.jackson.core.JsonProcessingException; | ||
import com.fasterxml.jackson.core.type.TypeReference; | ||
import com.fasterxml.jackson.databind.ObjectMapper; | ||
import jakarta.annotation.PostConstruct; | ||
import jakarta.servlet.http.HttpServletRequest; | ||
import org.springframework.beans.InvalidPropertyException; | ||
import org.springframework.beans.factory.annotation.Autowired; | ||
import org.springframework.boot.context.properties.EnableConfigurationProperties; | ||
import org.springframework.security.authentication.BadCredentialsException; | ||
import org.springframework.security.core.Authentication; | ||
import org.springframework.security.core.GrantedAuthority; | ||
import org.springframework.security.core.authority.AuthorityUtils; | ||
import org.springframework.security.core.authority.SimpleGrantedAuthority; | ||
import org.springframework.stereotype.Service; | ||
|
||
import java.util.Collections; | ||
import java.util.List; | ||
import java.util.Optional; | ||
import java.util.Set; | ||
import java.util.stream.Collectors; | ||
|
||
/** | ||
* API authentication service responsible for determining the authentication outcome for a given client request. | ||
*/ | ||
@Service | ||
@EnableConfigurationProperties(AuthenticationProperties.class) | ||
public class ApiAuthenticationService { | ||
|
||
private final AuthenticationProperties authenticationProperties; | ||
|
||
private Set<ClientCredential> clientCredentials; | ||
|
||
@Autowired | ||
protected ApiAuthenticationService(AuthenticationProperties authenticationProperties) { | ||
this.authenticationProperties = authenticationProperties; | ||
} | ||
|
||
/** | ||
* Initialise a set of {@link ClientCredential} from those configured as a JSON string in the application | ||
* properties. | ||
*/ | ||
@PostConstruct | ||
private void initialise() { | ||
try { | ||
clientCredentials = new ObjectMapper().readValue(authenticationProperties.getAuthorizedClients() | ||
, new TypeReference<Set<ClientCredential>>(){}); | ||
} catch (JsonProcessingException e) { | ||
throw new RuntimeException(e); | ||
} | ||
|
||
if (clientCredentials.isEmpty()) throw new InvalidPropertyException(AuthenticationProperties.class, | ||
"authorizedClients", "At least one authorized client must be provided."); | ||
} | ||
|
||
/** | ||
* Authenticate the HTTP request, comparing the access token provided by the client against the list of authorized | ||
* client details configured in the application properties. | ||
* | ||
* @param request the HTTP request made to the API | ||
* @return {@link Authentication} outcome with the list of roles assumed by the client if successful | ||
* @throws BadCredentialsException when authentication fails | ||
*/ | ||
protected Authentication getAuthentication(HttpServletRequest request) { | ||
String accessToken = request.getHeader(authenticationProperties.getAuthenticationHeader()); | ||
if (accessToken == null || !isAuthorizedAccessToken(accessToken)) { | ||
throw new BadCredentialsException("Missing or invalid API access token"); | ||
} | ||
|
||
Optional<ClientCredential> clientCredential = getMatchingClientCredential(accessToken); | ||
|
||
List<GrantedAuthority> grantedAuthorities = getClientRoles(accessToken) | ||
.stream() | ||
.map(role -> "ROLE_" + role) | ||
.map(SimpleGrantedAuthority::new) | ||
.collect(Collectors.toUnmodifiableList()); | ||
|
||
if (grantedAuthorities.isEmpty()) grantedAuthorities = AuthorityUtils.NO_AUTHORITIES; | ||
|
||
return new ApiAuthenticationToken(getPrincipal(accessToken), accessToken, grantedAuthorities); | ||
} | ||
|
||
/** | ||
* Retrieve a list of roles associated with the client, based on the access token provided. If the client is | ||
* not in the authorized list, no roles are returned. | ||
* | ||
* @param accessToken the client-provided access token | ||
* @return the list of roles associated with the access token, if authorized | ||
*/ | ||
private Set<String> getClientRoles(String accessToken) { | ||
return getMatchingClientCredential(accessToken) | ||
.map(ClientCredential::roles) | ||
.orElse(Collections.emptySet()); | ||
} | ||
|
||
/** | ||
* Determine whether an access token is authorized. | ||
* | ||
* @param accessToken the client-provided access token | ||
* @return {@code true} if the access token is authorized and {@code false} otherwise. | ||
*/ | ||
private boolean isAuthorizedAccessToken(String accessToken) { | ||
return getMatchingClientCredential(accessToken) | ||
.isPresent(); | ||
} | ||
|
||
/** | ||
* Retrieve the principal (client name) based on the access token provided. | ||
* | ||
* @param accessToken the client-provided access token | ||
* @return the principal (client name) associated with the access token | ||
*/ | ||
private String getPrincipal(String accessToken) { | ||
return getMatchingClientCredential(accessToken) | ||
.map(ClientCredential::name) | ||
.orElse(null); | ||
} | ||
|
||
/** | ||
* Retrieve the client details based on the access token provided. | ||
* | ||
* @param accessToken the client-provided access token | ||
* @return the {@link ClientCredential} associated with the access token | ||
*/ | ||
private Optional<ClientCredential> getMatchingClientCredential(String accessToken) { | ||
return clientCredentials.stream() | ||
.filter(credential -> credential.token().equals(accessToken)) | ||
.findFirst(); | ||
} | ||
} |
34 changes: 34 additions & 0 deletions
34
...ot-starter-auth/src/main/java/uk/gov/laa/ccms/springboot/auth/ApiAuthenticationToken.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,34 @@ | ||
package uk.gov.laa.ccms.springboot.auth; | ||
|
||
import org.springframework.security.authentication.AbstractAuthenticationToken; | ||
import org.springframework.security.core.Authentication; | ||
import org.springframework.security.core.GrantedAuthority; | ||
|
||
import java.util.Collection; | ||
|
||
/** | ||
* The API {@link Authentication} token representing a successfully authenticated client. | ||
*/ | ||
public class ApiAuthenticationToken extends AbstractAuthenticationToken { | ||
|
||
private final String clientName; | ||
private final String accessToken; | ||
|
||
public ApiAuthenticationToken(String clientName, String accessToken, Collection<? extends GrantedAuthority> authorities) { | ||
super(authorities); | ||
this.clientName = clientName; | ||
this.accessToken = accessToken; | ||
this.setAuthenticated(true); | ||
} | ||
|
||
@Override | ||
public Object getCredentials() { | ||
return accessToken; | ||
} | ||
|
||
@Override | ||
public Object getPrincipal() { | ||
return clientName; | ||
} | ||
|
||
} |
Oops, something went wrong.