What seemed like a simple endeavor -- learn how to implement OAuth to protect web services and portal applications using Java and Spring -- turned out to be much more complicated than expected. The posts in this series are targeting two groups -- developers with little background in the goals and mechanics of OAuth and more experienced developers who just need help avoiding pitfalls resulting from inconsistencies in implementations of OAuth. The content is divided into multiple posts:
OAuth Integration - OverviewOAuth Integration - Challenges
OAuth Integration - Authorization Server
OAuth Integration - Command Line Validation
OAuth Integration - Resource Server<---- YOU ARE HERE
OAuth Integration - Service Client
OAuth Integration - User Portal Application
Task List for Resource Server Implementation
Components acting as Resource Servers are responsible for accepting a JWT token with incoming requests, validating the integrity of the token (structure, unaltered content and expiration) then mapping identity information derived from the token to internal role information need to authorize or block execution of the requested endpoint. Resource Server components are NOT responsible for renewing expired tokens -- logic for that is implemented by clients and will be discussed in subsequent sections.
Developers implementing Resource Server protections for components will tackle the following tasks, shown in some semblance of build / dependency order.
- Add the dependencies required for OAuth2, JSON and JWT functions to the pom.xml file for the project.
- Devise a scheme for providing configuration information needed for token validation and updating application.properties with new entries. NOTE -- this solution will use a mix of "factory" settings used by Spring libraries with additional attributes that proved to be easier to use in implementing consistent functionality for multiple Authorization Server implementions.
- Creating a class to house logic performing signature validation and expiration validation on incoming tokens.
- Creating a class to house logic to transform external userid / role information to internal Java Authentication objects used by core Spring libraries and map external non-standard role information to a uniform internal model.
- Create a SecurityConfiguration class that will house various @Bean methods that hook methods in the above validation and authorization classes into appropriate extension points in the Spring Filter Chain security flow.
- Within the SecurityConfiguration class, code a method returning a reference to a NimbusJwtDecoder objected configured to use security certificates from the desired Authorization Server endpoint.
- Within the SecurityConfiguration class, code a filterChain(HttpSecurity) method to configure various aspects of the Spring Security FilterChain and insert the custom mdhlabsJwtFilterBean component.
- Use @PreAuthorize and @PostAuthorize annotations as needed in the application Controller classes to apply desired authorization restrictions to the underlying methods implementing each incoming URI call.
![]() |
Before beginning or resuming any development involving OAuth functionality in Java, take time to review the precautions summarized in section 2.4 of this document regarding deprecated classes and use of legacy ServletContext versus newer WebServletContext classes. |
The process of protecting a Spring Boot web service via Oauth JWT tokens will be described in the sections below using a typical REST type Spring Boot web service and Keycloak as the OAuth Authorization Server component. The existing source code structure of the Depends service is recapped below with directory-only rows deleted for brevity.
mdh@fedora1 ~/gitwork/oauthdependsresource]$ find src/main
src/main/java/com/mdhlabs/depends/dao/ProjectsDAO.java
src/main/java/com/mdhlabs/depends/services/ProjectController.java
src/main/java/com/mdhlabs/depends/models/Project.java
src/main/java/com/mdhlabs/depends/DependsConfiguration.java
src/main/java/com/mdhlabs/depends/SecurityConfig.java
mdh@fedora1:~/gitwork/oauthdependsresource $
5.2 Code Modularization Strategy
The solution design reflected in the example being built here is influenced by the following goals:
- providing an intuitive configuration model that is consistent for any arbitrary Authorization Server
- minimizing or avoiding entirely any changes to "core code" of a typical web service
- use standard extension points in existing Spring classes where doing so retains the ability to support multiple providers with a single processing model
- isolation of JWT validation / transformation to classes outside the core application to allow packaging as a standalone module to allow plug-in re-use in multiple services and applications
The solution design also reflects one CRUCIAL choice a developer must make for any implementation based on Java and Spring Security. As referenced in the Spring Evolution section of the Overview, the Security Filter Chain became the directional design beginning in Spring Security 5.4. That approach passes incoming requests through a LARGE number of sequential filters, each looking at the same request / response variables but optimized for specific lifecycle tasks of authentication / authorization. Spring Security includes a filter called BearerTokenAuthenticationFilter
which includes configurations that support custom classes to perform validations and transformatiions. Developers can extend BearerTokenAuthenticationFilter
OR they can create their own custom filter class extending GenericFilterBean
and insert that into the filter chain to perform the same validations and transformations with whatever class signatures they desire.
![]() |
Having tried both approaches, the custom filter approach does pose one possible challenge. Configuring centralized Http layer security restrictions seems to rely upon an Authentication object be provided by BearerTokenAuthenticationFilter and will not see the Authentication object set into the SecurityContext by a custom filter, even if it is inserted prior to BearerTokenAuthenticationFilter in the chain. If you only plan on use method-level security, this won't be a problem. If you wish to use centralized Http security, more experimentation may be required to use a custom filter. |
The example built in this section will use defined extension points for BearerTokenAuthenticationFilter
, which dictates the modularization and class signatures of some of the components below.
Class | Purpose / Contents |
mdhabsAuthenticationFailureHandler | Implements AuthenticationEntryPoint to allow customization of outgoing error messages when client credentials fail basic validation. |
mdhlabsAuthorizationFailureHandler | Implements AccessDeniedHandler to allow customization of outgoing error messages when client credentials lack authorization to execute requested functions. |
mdhlabsBearerTokenResolver | Implements BearerTokenResolver to provide a method for extracting bearer tokens from incoming requests |
mdhlabsJwtAuthenticationConverter | Implements a generic Converter<Sourcetype, Targettype> class to convert an AbstractAuthenticationToken (an external token) to a Jwt with user / authorization information standardized for internal use. |
JwtValidationUtilities | Implements a collection of methods for validating the signature / expiration of a JWT, signature generation, signature validation and methods for creating keypairs and public keys from a variety of specification formats. |
Failure Handlers and BearerTokenResolver Logic
The AuthenticationFailureHandler
, AuthorizationFailureHandler
and BearerTokenResolver
classes are the most straightforward tasks in the entire effort of adopting Resource Manager functionality. Each class implements a generic class used by Spring Security to allow custom logic to be plugged into the larger BearerTokenAuthenticationFilter
process provided by generic Spring Security. Each class is very brief and nearly self explanatory.
![]() |
The AuthenticationFailureHandler class extends AuthenticationEntryPoint which implies it should handle logic invoked at the BEGINNING of any authentication attempt via user interactions or tokens. However, in reality, the implementation point is AFTER the point where earlier processes determine that authentication has failed. Confusing class name being implemented but the functionalty is simple. |
The two FailureHandler classes are nearly identical. Here is the source for the mdhlabsAuthenticationFailureHandler
class.
package com.mdhlabs.oauthsecurity;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import org.springframework.http.HttpStatus;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.security.core.AuthenticationException;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.Map;
import java.util.HashMap;
import java.util.Calendar;
import java.io.IOException;
import com.fasterxml.jackson.databind.ObjectMapper;
@Component
public final class mdhlabsAuthenticationFailureHandler implements AuthenticationEntryPoint {
private final Logger thisLog = LoggerFactory.getLogger(this.getClass().getCanonicalName());
@Override
public void commence(HttpServletRequest request, HttpServletResponse response,
AuthenticationException exception) throws IOException, ServletException {
ObjectMapper objectMapper = new ObjectMapper();
response.setStatus(HttpStatus.UNAUTHORIZED.value());
Map data = new HashMap<>();
data.put("timestamp", Calendar.getInstance().getTime());
data.put("exception", exception.getClass());
data.put("message", exception.getMessage());
response.getOutputStream().println(objectMapper.writeValueAsString(data));
thisLog.error("Exception=" + exception.getClass() + " message=" + exception.getMessage());
}
}
Here is the source for the mdhlabsAuthorizationFailureHandler
class.
The mdhlabsBearerTokenResolver
class implements a somewhat confusingly named parent class called BearerTokenResolver
. In typical Java development frameworks, a "Resolver" chooses AMONG subtending classes to perform work for a specific object based upon some characteristic within the object. Here, the "Resolver" just does the work. For tokens, this implementation class just attempts to find a token in an Authorization header of the request or as a cookie object named access_token. It isn't responsible for validating it or transforming it, just plucking it out of the HttpServletRequest and returning it up the chain.
package com.mdhlabs.oauthsecurity;
import org.springframework.security.oauth2.server.resource.web.BearerTokenResolver;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.Cookie;
import org.springframework.util.StringUtils;
@Component
public class mdhlabsBearerTokenResolver implements BearerTokenResolver {
private final Logger thisLog = LoggerFactory.getLogger(this.getClass().getCanonicalName());
public String resolve(HttpServletRequest request) {
thisLog.debug("resolve() - extracting base64jwt from HttpServletRequest");
//-------------------------------------------------------------------------------------------
// for maximum re-usability, check for token sent as header "Authorization: Bearer $token"...
//-------------------------------------------------------------------------------------------
String bearerToken = request.getHeader("Authorization");
if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) {
String jwt = bearerToken.substring(7, bearerToken.length());
// thisLog.debug("resolve() - found in Authorization: header - jwt=" + jwt);
thisLog.debug("resolve() - found in Authorization: header - jwt=NOTSHOWINGFULLSTRING" );
return jwt;
}
//-----------------------------------------------------------------------------------------
// ...or see if it was sent in an access_token cookie from a browser / servlet application
//------------------------------------------------------------------------------------------
//thisLog.debug("resolveToken() - attempting to extract token from access_token cookie");
String token = "";
// get token from a Cookie
Cookie[] cookies = request.getCookies();
if (cookies == null || cookies.length < 1) {
thisLog.debug("resolve() -- no cookies submitted with request - returning null");
return null;
}
Cookie sessionCookie = null;
for (Cookie cookie : cookies) {
// thisLog.debug("attemptAuthentication() -- saw cookie name=" + cookie.getName());
if(("access_token").equals(cookie.getName())) {
thisLog.debug("resolve() -- found access_token=" + cookie.getValue());
// thisLog.debug("resolve() -- found access_token=NOTSHOWINGFULLSTRING");
return cookie.getValue();
}
}
// if here, we didn\\'t find a cookie named access_token so return null
return null;
}
}
Configuration via application.properties Entries
Validating external tokens and transforming them into internal Authentication objects across possibly multiple external providers requires a significant number of "this 2 that" mappings between provider names, parameter names and external / internal values for parameters. The validations and mappings described in the Overview section can be accommodated by a collection of configuration parameters that can be parsed and added to a series of HashMaps to allow transformations to be performed by simple "get" operations against these HashMaps.
A custom scheme will be devised for most of the validation and role mapping tasks and some standard configuration values expected by Spring libraries are described in the sections below.
Configuration Design for Unique mdhlabs Implementation Properties
The public key information comes from each Authorization Server but that is NOT information that should be fetched for each request since it should not change often. It makes sense to query that from each server when detected in configuration entries at startup then house in local cache for rapid use. A careful read of all of the task bullets above directly suggests a design for handling these parameter transformations that is easy to configure for administrators, easy to grasp for developers and highly performant.
- create a HashMap mapping issuer to provider (issuer2provider)
- create a HashMap mapping provider to jwkseturi (provider2jwkseturi)
- create a HashMap mapping provider to jwkset (provider2jwkset)
- create a HashMap mapping provider to username (provider2username)
- create a HashMap mapping provider to a role mapping (provider2mapping)
- create a HashMap mapping kid (key id) to public key (kid2publickey)
- create a method that reads in application.properties entries, parses the key and puts a (key,value) entry into the appropriate this2that HashMap
- create a method that loops through the provider2jwkseturi entries and fetches content from the specified URI and puts the (provider, jwkset) entry into provider2jwkset then generates the public key for each key set entry and puts a (kid,publickey) entry in kid2publickey
Here is what the custom configuration properties would look like for two Authorization Server providers, one named mdhlabs and one named providerx.
# these define role mappings from various OAuth Authorization Server providers above
# and where to locate the provider roles in their token response payloads
com.mdhlabs.oauth.mapping.mdhlabs=mdhlabsgetonly:customer,mdhlabsfull:engineer
com.mdhlabs.oauth.rolepath.mdhlabs=roles
com.mdhlabs.oauth.issuer.mdhlabs=http://192.168.99.10:8011/realms/mdhlabs
com.mdhlabs.oauth.username.mdhlabs=preferred_username
com.mdhlabs.oauth.jwkseturi.mdhlabs=http://192.168.99.10:8011/realms/mdhlabs/protocol/openid-connect/certs
# adding these to test unique solution logic that may support multiple Authorization Servers
# these define role mappings from various OAuth Authorization Server providers above
# and where to locate the provider roles in their token response payloads
com.mdhlabs.oauth.mapping.providerx=providerxbasic:customer,providerxadvanced:admin,mdhlabsuser:customer
com.mdhlabs.oauth.rolepath.providerx=roles
com.mdhlabs.oauth.issuer.providerx=http://192.168.99.10:8011/realms/providerx
com.mdhlabs.oauth.username.providerx=preferred_username
com.mdhlabs.oauth.jwkseturi.providerx=http://192.168.99.10:8011/realms/providerx/protocol/openid-connect/certs
The HashMap variables to house these mappings should be defined as gobal public variables in a class as shown here.
public static HashMap issuer2provider = new HashMap<>();
// yup, we need this mapping in both directions -- not all providers recap their issuer
// in their access_tokens
public static HashMap provider2issuer = new HashMap<>();
public static HashMap provider2username = new HashMap<>();
public static HashMap> provider2role = new HashMap<>();
public static HashMap provider2path = new HashMap<>();
public static HashMap provider2scopes = new HashMap<>();
public static HashMap algorithm2bytes = new HashMap<>();
public static HashMap> provider2claimmap = new HashMap<>();
public static HashMap provider2jwkseturi = new HashMap<>();
public static HashMap provider2jwkset = new HashMap<>();
public static HashMap kid2publickey = new HashMap<>();
Here is what each configuration element defines:
provider | internal shorthand name for each unique Authorization Server | issuer | primary URL of the Authorization Server issuing an access token | jwkseturi | the URL of service on the Authorization Server that will return a JSON encoded list of all public keys in service on the Authorization server, each identified by a kid ("key id") value | username | identifies the name of the claim in the JWT body that will be used as the username in the internal Authentication objec | rolepath | identifies the name of the claim in the JWT body that will be used as the external role information of the user that will be transformed into internal role string | mapping | provides the externalrole:internalrole mappings to use for an Authorization Server provider |
A typical code block for reading and interating through all configuration entires in application.properties looks like this.
try {
InputStream propertiesInput = this.getClass().getClassLoader().getResourceAsStream("application.properties");
Properties appProperties = new Properties();
appProperties.load(propertiesInput);
@SuppressWarnings("unchecked")
Enumeration propertyKeyEnum = (Enumeration) appProperties.propertyNames();
while (propertyKeyEnum.hasMoreElements() ) {
String key = propertyKeyEnum.nextElement();
String value = appProperties.getProperty(key);
//=============================================
// do something magic here with each (key,value)
//=============================================
}
}
catch (Exception theE) {
thisLog.error("initializeClient() -- failure loading properties -- Exception=" + theE );
allpresent=false;
}
Within that main loop, each (key,value) combination must be matched against the appropriate key prefix to identify which this2that HashMap needs to be updated. The logic for two of the maps is shown below.
if (key.startsWith("com.mdhlabs.oauth.jwkseturi.")) {
String provider = key.replaceAll("com.mdhlabs.oauth.jwkseturi.","");
provider2jwkseturi.put(provider, value);
}
if (key.startsWith("com.mdhlabs.oauth.issuer.")) {
String provider = key.replaceAll("com.mdhlabs.oauth.issuer.","");
// when used to decode tokens, we DO want this keyed by issuer_uri
issuer2provider.put(value, provider);
provider2issuer.put(provider, value);
}
Since these transformation lookups are reqired for each incoming request token, these methods and the HashMap
objects they populate will be implanted in the JwtAuthenticationConverter
class which will be covered in more detail below. The constructor method of the class will perform the loading of application.properties
entries and trigger the real-time fetch of public keys for each provider's defined jwkseturi endpoint.
Configuration for Spring Library OAuth Properties
For Resource Server implementations, Spring libraries assume only a single Authorization Server will be used for validation of JWT signatures. The property name to use to supply that parameter depends upon whether the application being built is being coded as a standalone servlet or for running within Spring Boot. The property entries for both are illustrated below and both can be included in a properties file but only one will be used based upon whether Spring Boot is in use.
#----------------------------------------------------------------------------------------------
# if this service only uses OAuth2 to protect itself using an OpenID Connect compliant
# Authorization Server (such as KeyCloak), this is the only endpoint configuration needed
#-----------------------------------------------------------------------------------------------
spring.security.oauth2.resourceserver.jwt.issuer-uri=http://192.168.99.10:8011/realms/mdhlabs
# the jwk-set-uri can be optionally provided to allow this service to start even if
# the Authorization Server is down at the time of startup
spring.security.oauth2.resourceserver.jwt.jwk-set-uri=http://192.168.99.10:8011/realms/mdhlabs/protocol/openid-connect/certs
# this parameter IS required for proper token validation within Spring Boot
security.oauth2.resource.jwk.key-set-uri=http://192.168.99.10:8011/realms/mdhlabs/protocol/openid-connect/certs
The above parameters allow token handling functions to recognize an Authorization Server referenced in an incoming token and where to get public key information from that Authorization Server to validate the integrity of incoming tokens to guard against tampering, expired tokens, etc. However, logic required to understand user authorizations requires information from Authorization Servers that has enormous variability in implementations making the out-of-the-box Spring implementation lacking.
![]() |
In these examples, the required properties will be specified in the application.properties file in the build. These settings would typically differ between development, test and production environments and thus would normally be something kept outside the final service binary archive. That isolation of configuration to external files will be left as a an exercise for the diligent student. |
![]() |
The Spring Security Libraries do not consistently scan properties and specifically identify required properties that are missing. Some parameter errors will crash the application at startup. Other parameter errors will result in null strings being passed at the time of invocation causing exceptions then. If such errors are encountered, CHECK VERSION NUMBERS of all dependencies in pom.xml and CHECK FOR TYPOS in the application.properties file. |
Introspect Configuration for Opaque Token Processing
As summarized in section 2.2.7, authorization information can be conveyed in a standard JWK token in which most / all of the parameters needed about the user and roles are supplied as "key":"value" claim pairs in the body of the JWT token. Some scenarios want or desire to limit exposure of user information as much as possible and instead provide an "opaque" token which provides a barebones token with signature / expiration parameters but instead of user details, the Authorization Server requires the access token and the client-id / client-secret to be submitted to a second endpoint to fetch the user details. If an Authorization Server uses opaque tokens, additional Spring configuration parameters must be provided as shown below to allow that $introspect endpoint to be invoked.
# these extra parms are required if using opaque access tokens - no harm
# if configured but not used
spring.security.oauth2.resourceserver.opaque.introspection-uri= http://192.168.99.10:8011/realms/mdhlabs/protocol/openid-connect/token/introspect
spring.security.oauth2.resourceserver.opaque.client-id=KuberDependsOauth
spring.security.oauth2.resourceserver.opaque.client-secret=9eL1t5XalxDL9GFrYBfoYrWAo0Jo1baH
![]() |
This example will not delve into handling opaque token introspection. The flow required in the Authorization server will be very similar to the userinfo flow implemented for the GUI client example later in this series. |
JWT Utilities / Validation Logic Development
Validation of incoming JWT payloads requires a wrapper method to combine the following steps.
- split the full token into header64url, body64url and signature64url
- convert header64url to headerjson
- convert body64url to bodyjson
- extract "kid" key id from headerjson and get the associated RSAPublicKey from kid2publickey HashMap
- concatenate header64url + . + body64url
- pass that combined string and the publickey to verifySignature(message, publickey) -- throw Exception on failure
- test for expiration by extracting "iat" and "exp" from bodyjson and comparing against Instant.now()
This logic will be implemented as validBase64token() which accepts a String value for the token requiring validation in its raw form directly out of the HttpServletRequest after extraction by mdhlabsBearerTokenResolver.
//---------------------------------------------------------------------------------
// validBase64token() - accepts a full (head/body/sig) base64 token and
// 1) splits into head/body/sig
// 2) converts head to json and extracts kid (key id)
// 3) converts body to json and extracts iat and exp (issued at / expires at)
// 4) gets key for kid from kid2publickey hashmap
// 5) validates sent signature against recalculated signature - REJECT if invalid
// 6) checks for expiration
//---------------------------------------------------------------------------------
public static String validBase64token(String base64token) {
thisLog.debug("validBase64token() =====================================================");
String result = "valid";
String tokenparts[] = base64token.split("\\.");
if (tokenparts.length != 3) {
thisLog.error("validBase64token() -- result=invalidHeaderBodySig");
return "invalidHeaderBodySig";
}
String sentmessage = tokenparts[0] + "." + tokenparts[1];
String sentsignature = tokenparts[2];
String kid = "";
String headerJson = "";
String bodyJson = "";
try {
headerJson = new String(Base64.decodeBase64(tokenparts[0]), "UTF-8");
bodyJson = new String(Base64.decodeBase64(tokenparts[1]), "UTF-8");
ObjectMapper mapper = new ObjectMapper();
JsonNode headernode = mapper.readTree(headerJson);
kid = headernode.path("kid").asText();
if (kid.equals("")) {
thisLog.error("validBase64token() -- result=invalidMissingkid");
return "invalidHeaderBodySig";
}
thisLog.debug("validBase64token() -- kid=" + kid);
RSAPublicKey publickey = mdhlabsJwtAuthenticationConverter.kid2publickey.get(kid);
if (publickey == null) {
thisLog.error("validBase64token() -- result=invalidMissingkid");
return "invalidWrongkid";
}
// thisLog.debug("validBase64token() -- publickey=" + publickey.toString());
byte[] publickeybytes = publickey.getEncoded();
// String publickey64 = Base64.encodeBase64String(publickeybytes);
String publickey64url = Base64.encodeBase64URLSafeString(publickeybytes);
// thisLog.debug("validBase64token() -- publickey64 =" + publickey64);
thisLog.debug("validBase64token() -- publickey64url =" + publickey64url);
// String sentsignature64 = sentsignature.replace('-','+');
// sentsignature64 = sentsignature64.replace('_','/');
thisLog.debug("validBase64token() -- sentsignature =" + sentsignature);
// thisLog.debug("validBase64token() -- sentsignature64=" + sentsignature64);
boolean validSignature = verify(sentmessage,sentsignature,publickey);
// boolean validSignature64 = verify(sentmessage,sentsignature64,publickey);
thisLog.debug("validBase64token() -- validSignature=" + validSignature);
if (validSignature == false) {
thisLog.error("validBase64token() -- result=invalidSignatureMismatch");
return "invalidSignatureMismatch";
}
// 6) check for expiration
JsonNode bodynode = mapper.readTree(bodyJson);
Instant issuedat = Instant.ofEpochSecond(bodynode.path("iat").asLong());
Instant expiresat = Instant.ofEpochSecond(bodynode.path("exp").asLong());
Instant now = Instant.now();
if (expiresat.compareTo(Instant.now()) < 0) {
thisLog.debug("validBase64token() -- result=invalidExpired expiresat=" + expiresat.toString() + " now="
+ now.toString());
return "invalidExpired";
}
}
catch (Exception theE) {
thisLog.error("validBase64token() - other error encountered -- Exception=" + theE);
return "invalidException";
}
thisLog.debug("validBase64token() - result=" + result);
return result;
}
Within those steps are references to other simple utilty functions also required for the lower level security key recovery, generation and verification. Since these methods are relatively straightforward, additional methods to create key pairs from external key specifications, generate signatures and enrypt/de-encrypt data will also be provided that will be useful for the GUI application example.
public static String sign(String plainText, PrivateKey privateKey) throws Exception {
Signature privateSignature = Signature.getInstance("SHA256withRSA");
privateSignature.initSign(privateKey);
privateSignature.update(plainText.getBytes(StandardCharsets.UTF_8));
byte[] signature = privateSignature.sign();
return Base64.encodeBase64URLSafeString(signature);
}
public static boolean verify(String plainText, String signature, PublicKey publicKey) throws Exception {
//thisLog.debug("verify() -- plaintText=" + plainText);
Signature publicSignature = Signature.getInstance("SHA256withRSA");
publicSignature.initVerify(publicKey);
publicSignature.update(plainText.getBytes(StandardCharsets.UTF_8));
byte[] signatureBytes = Base64.decodeBase64(signature);
return publicSignature.verify(signatureBytes);
}
public static String encrypt(String plainText, PublicKey publicKey) throws Exception {
Cipher encryptCipher = Cipher.getInstance("RSA");
encryptCipher.init(Cipher.ENCRYPT_MODE, publicKey);
byte[] cipherText = encryptCipher.doFinal(plainText.getBytes(StandardCharsets.UTF_8));
return Base64.encodeBase64URLSafeString(cipherText);
}
public static String decrypt(String cipherText, PrivateKey privateKey) throws Exception {
byte[] bytes = Base64.decodeBase64(cipherText);
Cipher decriptCipher = Cipher.getInstance("RSA");
decriptCipher.init(Cipher.DECRYPT_MODE, privateKey);
return new String(decriptCipher.doFinal(bytes), StandardCharsets.UTF_8);
}
public static KeyPair generateKeyPair() throws Exception {
KeyPairGenerator generator = KeyPairGenerator.getInstance("RSA");
generator.initialize(2048, new SecureRandom());
KeyPair pair = generator.generateKeyPair();
return pair;
}
public static PublicKey createPublicKeyFromJWKS(String jwksJson) {
try {
ObjectMapper mapper = new ObjectMapper();
JsonNode rootNode = mapper.readTree(jwksJson);
String exponent64url = rootNode.path("e").asText();
String nmodulus64url = rootNode.path("n").asText();
byte[] exponentBytes = Base64.decodeBase64(exponent64url);
byte[] nmodulusBytes = Base64.decodeBase64(nmodulus64url);
KeyFactory kf = KeyFactory.getInstance("RSA");
return kf.generatePublic(new RSAPublicKeySpec(new BigInteger(nmodulusBytes),new BigInteger(exponentBytes)));
}
catch (Exception theE) {
thisLog.error("createPublicKeyFromPublicCertString() - Exception=" + theE);
return null;
}
}
public static PublicKey createPublicKeyFromNEStrings(String n64url, String e64url) {
try {
byte[] exponentBytes = Base64.decodeBase64(e64url);
byte[] nmodulusBytes = Base64.decodeBase64(n64url);
KeyFactory kf = KeyFactory.getInstance("RSA");
// NOTE: the JWK spec returns n and e as negative numbers so BigInteger() must
// be called to set signmagnitude=1 (negative)
BigInteger nmodulusBigInteger = new BigInteger(1, nmodulusBytes);
BigInteger exponentBigInteger = new BigInteger(1, exponentBytes);
thisLog.debug("createPublicKeyFromNEStrings() - nmodulusBigInteger=" + nmodulusBigInteger
+ " exponentBigInteger=" + exponentBigInteger);
return kf.generatePublic(new RSAPublicKeySpec(nmodulusBigInteger, exponentBigInteger));
}
catch (Exception theE) {
thisLog.error("createPublicKeyFromNEStrings() - Exception=" + theE);
return null;
}
}
public static PublicKey createPublicKeyFromModExp(BigInteger modulus, BigInteger exponent) {
try {
KeyFactory kf = KeyFactory.getInstance("RSA");
return kf.generatePublic(new RSAPublicKeySpec(modulus,exponent));
}
catch (Exception theE) {
thisLog.error("createPublicKeyFromPublicCertString() - Exception=" + theE);
return null;
}
}
public static PublicKey createPublicKeyFromPublicCertString(String certstring, String algorithm) {
try {
byte[] keybytes = Base64.decodeBase64(certstring.getBytes());
X509EncodedKeySpec xkey = new X509EncodedKeySpec(keybytes);
KeyFactory kf = KeyFactory.getInstance(algorithm);
return kf.generatePublic(xkey);
}
catch (Exception theE) {
thisLog.error("createPublicKeyFromPublicCertString() - Exception=" + theE);
return null;
}
}
public static RSAPublicKey getPublicKeyFromString(String publicKey, String algorithm) throws NoSuchAlgorithmException, InvalidKeySpecException {
byte[] bytes = Base64.decodeBase64(publicKey.getBytes()); // should this be decodeBase64URL ?
KeyFactory keyFactory = KeyFactory.getInstance(algorithm);
return (RSAPublicKey) keyFactory.generatePublic(new X509EncodedKeySpec(bytes));
}
public static RSAPublicKey getPublicKeyFromx5c(String x5c) throws CertificateException {
try {
byte[] keyBytes = Base64.decodeBase64(x5c); // should this be decodeBase64URL ?
CertificateFactory fact = CertificateFactory.getInstance("X.509");
Certificate cer = fact.generateCertificate(new ByteArrayInputStream(keyBytes));
return (RSAPublicKey) cer.getPublicKey();
}
catch (Exception theE) {
thisLog.error("getPublicKeyFromx5c() - Exception=" + theE.toString());
return null;
}
}
Transforming Authorization Attributes (mdhlabsJwtAuthenticationConverter)
After logic in the prior section validates the integrity of an incoming JWT token, information about the identity and its external role information needs to be transformed into an internal Authentication object which itself is a collection of lower level objects. A visual summary of that desired final construct is shown below.
![]() |
It isn't clear why these object structures duplicate the authorities structure as both a standalone top level object in the BearerAuthenticationToken and as a sub-object within the principal object. |
A crucial point reflected in this diagram is that the external bearer token submitted by a client is NOT the object passed between internal modules of an application used for authorization of the request. The userid and authorities reflected in the incoming bearer token must be converted into objects used by Spring Security and the underlying application being protected.
![]() |
It helps to think of Authentication as a noun rather than a physical process / verb. The user is not re-authenticating each time this Authentication object is created with each incoming request. |
As noted in the prior two sections, earlier incarnations of Spring Security and OAuth2 functionality provided PrincipalExtractor and AuthoritiesExtractor as extensible classes to extract an identity (typically username or sub or preferred_username) and claims from an external OAuth2 access token and convert them into internal elements depicted in the Authentication object above. However, these older methodologies were deprecated as of Spring 5.4 and should NOT be used.
Instead, a custom class named mdhlabsJwtAuthenticationConverter
will be created that implements Converter<FromClass,ToClass>
which can be provided as a @Bean
reference to be invoked by the BearerTokenAuthenticationFilter
in the main security chain. This custom class will perform multiple functions:
- at application startup, populate a collection of HashMaps that aid in simplifying lookups and mappings needed for the external to internal transformations
- at application startup, fetch the key sets from all defined Authorization Servers seen in the prior configuration load and put them as entries in the kid2publickey HashMap
- implement an
convert(Jwt thejwt)
method with @Override annotation to provide the standard interface for incoming token conversions that returns a populatedAbstractAuthenticationToken
object - implement a helper method
extractUserRoles()
that performs the configured mapping of external role strings to internal role strings - implement a helper method
extractAuthenticationFromMap()
that converts claims from the external JWT to aCollection
ofGrantedAuthority
objects, creates thePrincipal
andCredential
objects then returns all of them as anAuthentication
object returned throughconvert()
.
Configuring the Spring SecurityFilterChain
Some of the OAuth2 functionality required of a process acting as a Resource Server will be discovered and configured into processing flows via the various @Bean and @Component annotations used in various classes. However, unique scenarios will inevitiably require some behaviors to be explicitly configured, either via application.properties files or calls made by an application at startup as specified in a configuration class. The recommended approach for creating this configuration class has changed significantly as of Spring Security 5.4.
Deprecated Approach | Creating a SecurityConfiguration class extending WebSecurityConfigurerAdapter that includes a configure(HttpSecurity http) method with @Override that calls submethods of that HttpSecurity object to configure login protections, oauth2resourceServer behavior, etc. |
Directional Approach | Create a generic SecurityConfiguration class with @Configuration and @EnableWebSecurity that defines a filterChain(HttpSecurity http) method virtually identical to what would have been configured in the configure(HttpSecurity http) method in the old style. |
![]() |
These two approaches are mutually incompatible with one another. If any lower level custom classes or methods used to build either one of these configuration classes uses classes from the other, VERY CONFUSING compile-time or run-time errors will occur. Specifically, if using the SecurityChainFilter approach, any use of classes present in spring-security-oauth2 libraries will pull in other "Spring Security OAuth2" functionality that is DEPRECATED and will conflict with SecurityChainFilter functionality. In particular, at startup, the exception Found WebSecurityConfigurerAdapter as well as SecurityFilterChain. Please select just one.. may appear. To get rid of it, ensure no classes are loading classes found in spring-security-oauth2 package and remove that package from the pom.xml project file. |
The security flow that will be created when this SecurityConfigurationSFC class executes is illustrated below. There are direct correlations between many of the flow arrows and configuration statements within the class.
Sample code for using the SecurityFilterChain
approach is shown below in a class named SecurityConfigurationSFC
(the SFC being a reminder of the technique used).
package com.mdhlabs.depends; import com.mdhlabs.oauthsecurity.*; import com.mdhlabs.oauthsecurity.JwtValidationUtilities; import com.mdhlabs.oauthsecurity.mdhlabsJwtAuthenticationConverter; import java.util.*; import javax.servlet.http.HttpServletRequest; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.boot.autoconfigure.security.oauth2.resource.OAuth2ResourceServerProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.Configuration; import org.springframework.core.convert.converter.Converter; import org.springframework.http.HttpMethod; import org.springframework.security.authentication.AbstractAuthenticationToken; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.AuthenticationManagerResolver; import org.springframework.security.authentication.BadCredentialsException; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; import org.springframework.security.config.http.SessionCreationPolicy; import org.springframework.security.core.Authentication; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.oauth2.jwt.Jwt; import org.springframework.security.oauth2.jwt.JwtDecoder; import org.springframework.security.oauth2.jwt.NimbusJwtDecoder; import org.springframework.security.oauth2.server.resource.web.BearerTokenResolver; import org.springframework.security.web.access.AccessDeniedHandler; import org.springframework.security.web.authentication.AuthenticationConverter; import org.springframework.security.web.authentication.AuthenticationFilter; import org.springframework.security.web.AuthenticationEntryPoint; import org.springframework.security.web.SecurityFilterChain; //------------------------------------------------------------------------ // CLASS SecurityConfigureSFC - uses SecurityFilterChain configuration approach // defines helper beans used by filters in the security chain to analyze tokens // of incoming reqests to validate token integrity and perform any required // security blocks before forwarding the request to the servlet controller //------------------------------------------------------------------------ @Configuration @EnableWebSecurity @EnableGlobalMethodSecurity(prePostEnabled = true) @ComponentScan("com.mdhlabs") public class SecurityConfigurationSFC { private final Logger thisLog = LoggerFactory.getLogger(this.getClass().getCanonicalName()); // ------------------------------------------------------------------------------- // getmdhlabsAuthenticationFailureHandler() - returns an instance of a custom // AuthenticationFailureHandler named mdhlabsAuthenticationFailureHandler // that provides some control of how much detail is returned to clients // encountering authentication failures // ------------------------------------------------------------------------------- @Bean public AuthenticationEntryPoint getAuthenticationFailureHandler() { thisLog.info("getAuthenticationFailureHandler() - instantiating mdhlabsAuthenticationFailureHandler"); return (AuthenticationEntryPoint) new mdhlabsAuthenticationFailureHandler(); } //--------------------------------------------------------------------------- // getAuthorizationFailureHandler() - stub method to return reference // to custom mdhlabsAuthorizationFailureHandler -- for insufficient permission // errors //---------------------------------------------------------------------------- @Bean public AccessDeniedHandler getAuthorizationFailureHandler() { thisLog.debug("getAuthorizationFailureHandler() - instantiating mdhlabsAuthorizationFailureHandler"); return new mdhlabsAuthorizationFailureHandler(); } //--------------------------------------------------------------------------- // getmdhlabsBearerTokenResolver() - stub method to return reference // to custom mdhlabsBeaerTokenResolver() which extracts raw base64 token from // an HttpServetRequest object //---------------------------------------------------------------------------- @Bean public BearerTokenResolver getBearerTokenResolver() { thisLog.debug("getBearerTokenResolver() - instantiating mdhlabsBearerTokenResolver"); return new mdhlabsBearerTokenResolver(); } //-------------------------------------------------------------------------- // validMdhlabsAuthentication() - accepts an Authentication which houses a // Principal which will contain a base64token previously extracted by our // mdhlabsBearerTokenResolver class (resolve() method). We can inspect // that here and confirm if it is a token type we can process. For now, // we will assume TRUE as long as it isn't null. //-------------------------------------------------------------------------- private String validMdhlabsAuthentication(Authentication authentication) { //thisLog.debug("validMdhlabsAuthentication() - testing Authentication=" + authentication.toString()); thisLog.debug("validMdhlabsAuthentication() - testing Authentication=MASKEDFORBREVITY"); String base64jwt = (String) authentication.getPrincipal(); //thisLog.debug("validMdhlabsAuthentication() - base64jwt=" + base64jwt); String accessStatus = JwtValidationUtilities.validBase64token(base64jwt); thisLog.debug("validMdhlabsAuthentication() - result=" + accessStatus); return accessStatus; } //-------------------------------------------------------------------------- // mdhlabsAuthenticationManager() - returns a UsernamePasswordAuthenticationToken // object if the implicitly supplied Authentication matches our criteria //--------------------------------------------------------------------------- private AuthenticationManager mdhlabsAuthenticationManager() { thisLog.debug("mdhlabsAuthenticationManager() - testing incoming Authentication object"); return authentication -> { String authState = validMdhlabsAuthentication(authentication); if (authState.equals("valid")) { String base64jwt = (String) authentication.getPrincipal(); UsernamePasswordAuthenticationToken internalAuthentication = mdhlabsJwtAuthenticationConverter.extractAuthenticationFromJwtBase64(base64jwt); return internalAuthentication; } throw new BadCredentialsException(authState); }; } //------------------------------------------------------------------------- // getJwtAuthenticationConverter -- simple @Bean returning a reference to // a custom Converter used to convert external non-standard JWTs into // a consistent internal Authentication object for use by core Spring // Security handlers //------------------------------------------------------------------------- @Bean public ConvertergetJwtAuthenticationConverter() { thisLog.debug("getAuthenticationConverter() - instantiating mdhlabsAuthenticationConverter object"); return new mdhlabsJwtAuthenticationConverter(); } //------------------------------------------------------------------------- // mdhlabsAuthenticationManagerResolver() - simple stub that returns the // reference to mdhlabsAuthenticationManager() for handling incoming tokens // In general, this can be used to select BETWEEN different AuthenticationManager // classes if the distinction is based upon the request URI or something that // can be parsed from the request - we might extend this to parse the token // then use the issuer ("iss") to select which AuthenticationManager to use. //------------------------------------------------------------------------- public AuthenticationManagerResolver mdhlabsAuthenticationManagerResolver() { thisLog.debug("mdhlabsAuthenticationResolver() - returning reference to mdhlabsAuthenticationManager for token processing"); return request -> { // if (request.getPathInfo().startsWith("/employee")) { // return employeesAuthenticationManager(); // } // return customersAuthenticationManager(); return mdhlabsAuthenticationManager(); }; } //------------------------------------------------------------------------- // getmdhlabsAuthenticationFilter() - returns an AuthentictionFilter referencing // custom AuthenticationManagerResolver and AuthenticationConverter objects //-------------------------------------------------------------------------- private AuthenticationFilter getmdhlabsAuthenticationFilter() { thisLog.debug("getAuthenticationFilter() - instantiating new mdhlabsAuthenticationResolver / mdhlabsAuthenticationConverter"); AuthenticationFilter filter = new AuthenticationFilter( (AuthenticationManagerResolver ) mdhlabsAuthenticationManagerResolver(), (AuthenticationConverter) getJwtAuthenticationConverter() ); filter.setSuccessHandler((request, response, auth) -> { }); return filter; } //------------------------------------------------------------------------------- // encoder() - returns an instance of library function BCryptPasswordEncoder() // for use in creating User objects -- not sure this is needed for our scenerio // where we aren't authenticating human passwords but is temprorarily used // in another method call //------------------------------------------------------------------------------- @Bean public PasswordEncoder encoder() { thisLog.info("encoder() -- instantiation for BCryptPasswordEncoder "); return new BCryptPasswordEncoder(); } //----------------------------------------------------------------------------------- // getmdhlabsJwtFilterBean() - returns new object instance of mdhlabsJwtFilter to // be inserted in the SecurityFilter chain via addFilterAfter() below. //----------------------------------------------------------------------------------- /* DISABLE FOR NOW @Bean public mdhlabsJwtFilter getmdhlabsJwtFilter() { return new mdhlabsJwtFilter(); } */ //----------------------------------------------------------------------------- // jwtDecoder() - instantiates a JWT decoder using the JWKsetUri of an OAuth // provider to fetch strings to decode / unencrypt a token // NOTE -- not clear how this approach works when a single app can use // multiple OAuth providers for google, facebook, github, keycloak, etc // For now, this is hardcoding the JwkSetUri for a local keycloak instance. //----------------------------------------------------------------------------- @Bean public JwtDecoder jwtDecoder(OAuth2ResourceServerProperties properties) { thisLog.debug("jwtDecoder() - instantiating - incoming OAuth2ResourceServerProperties=" + properties.toString()); thisLog.info("jwtDecoder() - instantiating NimbusJwtDecoder pointing to Keycloak jwk-set-uri=" + properties.getJwt().getJwkSetUri()); NimbusJwtDecoder thisDecoder = NimbusJwtDecoder.withJwkSetUri(properties.getJwt().getJwkSetUri()).build(); return thisDecoder; } //----------------------------------------------------------------------------------- // filterChain(HttpSecurity) - the primary method in the SecurityFilterChain based approach // for defining security rules -- this is the new equivalent of the older // configure(HttpSecurity) method when using the WebSecurityConfigurerAdapter model //----------------------------------------------------------------------------------- @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { //------------------------------------------------------------------------------------------------ // NOTE: These patterns are APPENDED to the servlet context /depends/gui in application.properties //------------------------------------------------------------------------------------------------ thisLog.info("configure(HttpSecurity) - defining access filters for application URI patterns"); http .httpBasic().disable() .formLogin(AbstractHttpConfigurer::disable) .csrf(AbstractHttpConfigurer::disable) // NOTE: use newer .authorizeHttpRequests instead of older .authorizeRequests .authorizeHttpRequests(authorize -> authorize // NOTE: use .mvcMatchers instead of .antMatchers .mvcMatchers(HttpMethod.GET, "/project/*").hasAnyAuthority("customer", "engineer") // .mvcMatchers(HttpMethod.GET, "/project/*").denyAll() .mvcMatchers(HttpMethod.GET, "/projects*").hasAuthority("engineer") // .mvcMatchers(HttpMethod.POST, "/project").hasAuthority("engineer") // .mvcMatchers(HttpMethod.PUT, "/project/**").hasAuthority("engineer") .anyRequest().authenticated() ); // when configuring .oauth2ResourceServer(), you must either enable clear-text JTW // processing via .jwt() or opaque token processing via .opaqueToken() // if you configure .authenticationManagerResolver() under oauth2ResourceServer(), then // the .jwt() level configurations cannot be used http.oauth2ResourceServer() .authenticationEntryPoint(getAuthenticationFailureHandler()) .accessDeniedHandler(getAuthorizationFailureHandler()) .authenticationManagerResolver(mdhlabsAuthenticationManagerResolver()) .bearerTokenResolver(this.getBearerTokenResolver() ) // .jwt() // .authenticationManager(AuthenticationManager mdhlabsAuthenticationManager // .jwtAuthenticationConverter(getmdhlabsJwtAuthenticationConverter() ) // .decoder(myJwtDecoder() ) ; //--------------------------------------------------------------------------- // here is a full illustration of all oauth2ResourceServer() configurations //--------------------------------------------------------------------------- //http.oauth2ResourceServer() // .accessDeniedHandler(AccessDeniedHandler theADH) // .authenticationEntryPoint(AuthenticationEntryPoint theAEP) // .authenticationManagerResolver(AuthenticationManagerResolver theARH) // .bearerTokenResolver(BearerTokenResolver theBTR) // .jwt() // .authenticationManager(AuthenticationManager theAM) // .decoder(JwtDecorer theDecoder) // .jwkSetUri(String uri) // .jwtAuthenticationConverter(Converter theJTC) // .and() // .opaqueToken() // .authenticationManager(AuthenticationManager mgr) // .introspectionClientCredentials(String clientid, String clientsecret) // .introspectionUri(String uri) // .introspector(OpaqueTokenIntrospector inspector) http.sessionManagement(sessionManagement -> sessionManagement.sessionCreationPolicy(SessionCreationPolicy.STATELESS) ); return http.build(); } } // end of SecurityConfig class
Defining Authorization Restrictions for Services
So far, the web service has been altered to require client AUTHENTICATION via an access token obtained from the Authorization Server (Keycloak in this case) mutually trusted by the service and the client. To properly restrict an authenticated user to only those functions they require, AUTHORIZATION must also be performed against each incoming request reaching the service. The OAuth implementation in Spring allows authorization rules to be defined at two different planes of service logic:
Centralized Security Configuration via URI -- Using the SecurityConfigurationSFC
class built previously, expressions can be applied to the OAuth configuration passed to the Spring Security FilterChain to identify roles / GrantedAuthorities required to execute specific URI endpoints.
Individual Controller Class Methods -- @PreAuthorize
and PostAuthorize
annotations can be added to method definitions within Controller classes providing the same matching rules for roles / GrantedAuthorities, etc.
Details on both approaches are provided below.
![]() |
Why bother supporting TWO layers of configuration for authorization restrictions? Isn't the Spring model purposely biased towards centralizing and simplifying configuration in fewer rather than more locations? If applied at the Spring Security layer, wouldn't that keep more unauthorized traffic out of the system earlier in the flow? GOOD QUESTIONS. The framework developers actually recommend using annotations in Controller classes at the method level. The rationale is that the filtering performed via a SecurityConfiguration class and antMatcher is literally only matching on an incoming request URI. It has no context for the actual SERVICE method being invoked later when the request is routed through the servlet dispatcher. As a result, if a @RequestMapping annotation in a controller class is altered to remap a service from /myapp/api/group1/thisendpoint to /myapp/api/group2/thatendpoint and the corresponding antMatcher in SecurityConfiguration is not adjusted to match, it is POSSIBLE access will suddenly be provided to the service on the new URI if /myapp/api/group2 matches some other existing configuration that permitted traffic. Perhaps the best reason for preferring use of @PreAuthorize annotations in controllers instead of antMatchers or mvcMatchers in a SecurityConfiguration class is that the filtering of requests through matcher patterns is SEQUENTIAL. The first condition that matches the URI to a permission will BYPASS any other antMatcher patterns. Accidentally adding a wildcard pattern PRIOR to more restrictive patters will SKIP those later patterns for any requests that match. |
Centralized Configuration of Authorization Restrictions
The general class built as SecurityConfigurationSFC
in these examples had a configure(HttpSecurity) method that looked like this:
//-----------------------------------------------------------------------------------
// configure(HttpSecurity) - the primary method in the SecurityFilterChain based approach
// for defining security rules -- this is the new equivalent of the older
// configure(HttpSecurity) method when using the WebSecurityConfigurerAdapter model
//-----------------------------------------------------------------------------------
@Override
public void configure(HttpSecurity http) throws Exception {
thisLog.info("filterChain(HttpSecurity) - defining access filters for application URI patterns");
http
.httpBasic().disable()
.formLogin(AbstractHttpConfigurer::disable)
.csrf(AbstractHttpConfigurer::disable)
// NOTE: use newer .authorizeHttpRequests instead of older .authorizeRequests
.authorizeHttpRequests(authorize -> authorize
// NOTE: use .mvcMatchers instead of .antMatchers
.mvcMatchers(HttpMethod.GET, "/project/*").hasAnyAuthority("customer", "engineer")
// .mvcMatchers(HttpMethod.GET, "/project/*").denyAll()
.mvcMatchers(HttpMethod.GET, "/projects*").hasAuthority("engineer")
// .mvcMatchers(HttpMethod.POST, "/project").hasAuthority("engineer")
// .mvcMatchers(HttpMethod.PUT, "/project/**").hasAuthority("engineer")
.anyRequest().authenticated()
);
// when configuring .oauth2ResourceServer(), you must either enable clear-text JTW
// processing via .jwt() or opaque token processing via .opaqueToken()
// if you configure .authenticationManagerResolver() under oauth2ResourceServer(), then
// the .jwt() level configurations cannot be used
http.oauth2ResourceServer()
.authenticationEntryPoint(getAuthenticationFailureHandler())
.accessDeniedHandler(getAuthorizationFailureHandler())
.authenticationManagerResolver(mdhlabsAuthenticationManagerResolver())
.bearerTokenResolver(this.getBearerTokenResolver() )
// .jwt()
// .authenticationManager(AuthenticationManager mdhlabsAuthenticationManager
// .jwtAuthenticationConverter(getmdhlabsJwtAuthenticationConverter() )
// .decoder(myJwtDecoder() )
;
http.sessionManagement(sessionManagement ->
sessionManagement.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
);
return http.build();
}
This code segment uses a variety of patterns that merit additional explanation and clarification.
PATTERN ONE -- Concatenated versus Distinct Calls. The HttpSecurity object is designed so that most of its methods return a reference to the object itself. Since Java's core syntax allows chaining object calls together such as
myString.trim().equals("NONPADDEDSTRING")
the methods for HttpSecurity
allow the different areas of subfunctionality to be configured with calls that appear to add onto one another. These two blocks of code are functionally identical:
http .httpBasic().disable() .formLogin(AbstractHttpConfigurer::disable) .csrf(AbstractHttpConfigurer::disable) ;
http.httpBasic().disable(); http.formLogin(AbstractHttpConfigurer::disable); http.csrf(AbstractHttpConfigurer::disable);
The resulting source code looks a bit cleaner but will function as if each call was a separate statement. In the hierarchy of methods / objects and submethods / subobjects, most are built to return a reference to the SAME object, allowing another sub-method to be called. Each object also has an and()
method which will instead return a reference to the parent object. In source code, this allows the and()
to essentially conclude a sub-level and return to the parent where a different sub-component can be configured. Whether this aids source readability will be left as a religious debate.
PATTERN TWO -- Use of Lambda Function Syntax Some of the methods of HttpSecurity accept a reference to another class used to implement a custom function. Others implement their own submethods that accept method references as an argument. Many of the examples online for methods like authorizeHttpRequest() or xxxxxxx() provide methods as arguments using lambda syntax which can be VERY confusing. In general, use of lambda syntax is helpful for defining static logic to perform a required transformation without having to declare a standalone class / method and bother with naming conventions and conflicts. However, it is not REQUIRED and there is always a "non-lamba" way to achieve the same result if the lambda syntax is too confusing.
As an example, these two code blocks are functionally identical:
Lambda Syntax:
http
.httpBasic().disable()
.formLogin(AbstractHttpConfigurer::disable)
.csrf(AbstractHttpConfigurer::disable)
// NOTE: use newer .authorizeHttpRequests instead of older .authorizeRequests
.authorizeHttpRequests(authorize -> authorize
// NOTE: use .mvcMatchers instead of .antMatchers
.mvcMatchers(HttpMethod.GET, "/project/*").hasAnyAuthority("customer", "engineer")
.mvcMatchers(HttpMethod.GET, "/projects*").hasAuthority("engineer")
.mvcMatchers(HttpMethod.POST, "/project").hasAuthority("engineer")
.mvcMatchers(HttpMethod.PUT, "/project/**").hasAuthority("engineer")
.anyRequest().authenticated()
);
Traditional Syntax:
http
.httpBasic().disable()
.formLogin(AbstractHttpConfigurer::disable)
.csrf(AbstractHttpConfigurer::disable)
.authorizeHttpRequests()
.mvcMatchers(HttpMethod.GET, "/project/*").hasAnyAuthority("customer", "engineer")
.mvcMatchers(HttpMethod.GET, "/projects*").hasAuthority("engineer")
.mvcMatchers(HttpMethod.POST, "/project").hasAuthority("engineer")
.mvcMatchers(HttpMethod.PUT, "/project/**").hasAuthority("engineer")
.anyRequest().authenticated()
;
PATTERN THREE -- antMatcher
versus mvcMatcher
Spring Security includes two classes -- AntMatcher
(older) MvcMatcher
(newer) -- for specifiying patterns of URI strings when matching incoming requests against desired permission rules. The MvcMatcher
functionality is viewed by most of the Spring community as being superior because its matching logic is identical to that used by @RequestMapping
functionality in servlet controller functions. Specifically, antMatcher("/this/path/method")
would NOT match on /this/path/methodx
or /this/path/method/submethod
. An invocation of mvcMatcher("/this/path/method")
WOULD match on those variants. In essence, mvcMatcher
casts a wider net for matches even without trailing wildcards. To avoid condition with matching most developers are already familiar with for @RequestMapping
, it is better to use mvcMatcher
. HOWEVER, example confiugrations on web sites abound with references to antMatcher()
. In general, any reference to antMatcher()
can be (should be) replaced with mvcMatcher()
.
PATTERN FOUR -- Matcher Rules on Roles and Authorities Both antMatcher()
and mvcMatcher()
provide identical submethods which can be concatenated to the match to further narrow an authorization.
hasRole() hasAnyRole() hasAuthority() hasAnyAuthority()
The functionality of these methods creates one area of potential confusion. Internally, as authority strings are converted from an external authentication token, Spring Security defaults to prefixing each string with ROLE_
so an external role mdhlabsgetonly
mapped to internal role customer
is actually saved as ROLE_customer
, etc. HOWEVER, logic in the hasRole()
and hasAnyRole()
methods will still see hasRole("customer")
as a match even when the underlying external authority was converted to ROLE_customer
. The hasAuthority()
and hasAnyAuthority()
methods do NOT add this equivalence. An exact match is required to satisfy the condition.
PATTERN FIVE -- authorizeRequests() versus authorizeHttpRequests() There are TWO functions in the Spring Security library for configuring authorizations at the URI level with overlapping, confusing names --- authorizeRequests()
and authorizeHttpRequests()
. The authorizeRequests()
incarnation is older and uses older concepts for making authorization decisions if more than one authorization provider must be supported, etc. The authorizeHttpRequests()
incarnation is new as of Spring Security 5.4 and its implementation is intended to use a streamlined AuthenticationManager
based process for customization required for multiple providers. The documentation also says using authorizeHttpRequests()
will use AuthorizationFilter
in the security filter chain rather than the older FilterSecurityInterceptor
. Searches for guidance on OAuth security with Spring will INEVITABLY land on pages referencing the older authorizeRequests()
methods which will drag in other, older deprecated coding and configuration practices which will complicate development and code maintenance going forward. Make sure authorizeHttpRequests()
is used.
Controller-Level Configuration of Authorization Restrictions
Instead of defining AUTHORIZATION restrictions in the central SecurityConfiguration class based on URI paths, it is possible to define within individual Controller classes implementing the methods tied to URI endpoints. This is actually the preferred approach for the reasons explained above in section 5.8.
A typical controller class is shown below using these two annotations atop two specific endpoints:
@PreAuthorize("hasRole('engineer')") @PreAuthorize("hasAnyRole('customer','engineer')")
These are two very simple examples showing how a given method can be restricted to one or multiple role strings mapped from the external access token to the internal Authentication object. The @PreAuthorize
annotation applies the rule BEFORE the method is invoked making it the obvious tool for BLOCKING access. However, if blocking critiera can only be tested AFTER a method is executed (but before a response is returned), the @PostAuthorize
annotation can be used in the exact same way.
The @PreAuthorize
and @PostAuthorize
functions are quite powerful and complex and can trigger off any content in the request or response object drafted to return to the client. Full documentation is available at:
https://docs.spring.io/spring-security/reference/servlet/authorization/expression-based.html
package com.mdhlabs.depends.services; import java.util.List; import java.sql.SQLException; import javax.ws.rs.core.MediaType; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.core.context.SecurityContext; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.oauth2.core.oidc.user.OidcUser; import org.springframework.security.oauth2.core.oidc.OidcUserInfo; import org.springframework.security.oauth2.jwt.Jwt; import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken; //import org.springframework.security.oauth2.server.resource.authentication.Jwt; import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer; import org.springframework.security.access.prepost.PreAuthorize; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.mdhlabs.depends.dao.ProjectsDAO; import com.mdhlabs.depends.models.Project; import com.mdhlabs.oauthsecurity.*; import java.lang.Exception; import org.springframework.ui.Model; @RestController @EnableResourceServer public class ProjectController { @Autowired private ProjectsDAO projectsDAO; private final Logger thisLog = LoggerFactory.getLogger(this.getClass().getCanonicalName()); //---------------------------------------------------------------------------------------- // projectList - returns an array of all project objects in the database // URI = /context/servletPath/projects/ // METHOD = GET //---------------------------------------------------------------------------------------- @PreAuthorize("hasRole('ROLE_mdhlabsfull')") @RequestMapping(value="/projects", method=RequestMethod.GET, produces=MediaType.APPLICATION_JSON) public @ResponseBody ListprojectList() throws SQLException, Exception { Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); String username = mdhlabsSecurityUtilities.getUsernameFromAuthentication(authentication); thisLog.info("QUERY action=projectList method=get username=" + username); List listOfProjects = projectsDAO.retrieveProjects(); thisLog.info("REPLY action=projectList method=get username=" + username); return listOfProjects; } //---------------------------------------------------------------------------------------- // projectRetrieve - returns the specified Project object // URI = /context/servletPath/project/{project_id} // METHOD = GET //--------------------------------------------------------------------------------------------------- // to access values in the request objects, add HttpServletRequest as a pararameter of the controller // method then use .getHeader("xxxx")-- if you want to manually set response headers, add // HttpServletRespose as a parameter of the controller method then use .setHeader("xxxxx",value) //-------------------------------------------------------------------------------------------------- @PreAuthorize("hasAnyRole('ROLE_mdhlabsgetonly','ROLE_mdhlabsfull')") @RequestMapping(value="/project/{project_id}", method=RequestMethod.GET, produces=MediaType.APPLICATION_JSON) public @ResponseBody Project projectRetrieve(@PathVariable("project_id") int project_id, HttpServletRequest theQuery,HttpServletResponse theReply) throws SQLException, Exception { Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); String username = mdhlabsSecurityUtilities.getUsernameFromAuthentication(authentication); thisLog.info("QUERY action=projectRetrieve method=get project_id=" + project_id + " username=" + username); String headerDatetimegmt = theQuery.getHeader("datetimegmt"); String headerTxRoot = theQuery.getHeader("txroot"); String headerClient = theQuery.getHeader("client"); thisLog.info("QUERY HTTPHEADERS datetimegmt=" + headerDatetimegmt + " txroot=" + headerTxRoot + " client=" + headerClient); Project thisProject = projectsDAO.retrieveProject(project_id); thisLog.info("REPLY action=projectRetrieve method=get project_id=" + project_id + " username=" + username); return thisProject; } //other methods omitted here for brevity } // end of class ProjectController
When using @PreAuthorize
or @PostAuthorize
protections, log messages will be generated at startup of the component reflecting any filter configured against methods in the controller class. The logs look like this:
When requests are BLOCKED because of permissions enforced at the method layer, error logs will look like this:
2022-08-14 16:17:50,756 DEBUG org.springframework.security.access.intercept.aopalliance.MethodSecurityInterceptor - Failed to authorize 2022-08-23 14:14:24,082 DEBUG org.springframework.security.access.intercept.aopalliance.MethodSecurityInterceptor - Failed to authorize ReflectiveMethodInvocation: public java.util.List com.mdhlabs.depends.services.ProjectController.projectList() throws java.sql.SQLException,java.lang.Exception; target is of class [com.mdhlabs.depends.services.ProjectController] with attributes [[authorize: '#oauth2.throwOnError(hasAuthority('engineer'))', filter: 'null', filterTarget: 'null']] 2022-08-23 14:14:24,117 ERROR com.mdhlabs.oauthsecurity.mdhlabsAuthorizationFailureHandler - Exception=class org.springframework.security.access.AccessDeniedException message=Access is denied
Accessing User Details Within Service Logic
It is likely that application logic may need to reference information about the authenticated user in downstream transactions or log events triggered by a request. Information about the current user will be housed as an Authentication
object inside the SecurityContext
of the request. A utility class named mdhlabsSecurityUtilities
can be provided to house a method for extracting the username from any Authentication
subclass and return it for use in service logic and logging.
package com.mdhlabs.oauthsecurity; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.security.core.context.SecurityContextHolder; //import org.springframework.security.core.context.SecurityContext; import org.springframework.security.core.Authentication; import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken; import org.springframework.security.oauth2.core.DefaultOAuth2AuthenticatedPrincipal; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.oauth2.core.oidc.user.OidcUser; import org.springframework.security.oauth2.core.oidc.OidcUserInfo; import org.springframework.security.oauth2.jwt.Jwt; public class mdhlabsSecurityUtilities { private static final Logger thisLog = LoggerFactory.getLogger("com.mdhlabs.oauthsecurity.mdhlabsSecurityUtilities"); //------------------------------------------------------------------------ // getUsernameFromAuthentication() -- extracts what should be the username // parameter from various types of incoming Authentication-derived objects //------------------------------------------------------------------------ public static String getUsernameFromAuthentication(Authentication auth) { Object currentPrincipal = auth.getPrincipal(); thisLog.debug("getUsernameFromAuthentication()-- " + currentPrincipal.toString()); String username; if (currentPrincipal instanceof UserDetails) { username = ((UserDetails) currentPrincipal).getUsername(); } else if (currentPrincipal instanceof OidcUser) { OidcUserInfo thisOidcUserInfo = ((OidcUser) currentPrincipal).getUserInfo(); thisLog.debug("DEBUG getUsernameFromAuthentication() - OidcUserInfo = " + thisOidcUserInfo.toString()); username = thisOidcUserInfo.getPreferredUsername(); } else if (currentPrincipal instanceof Jwt) { JwtAuthenticationToken authenticationToken = (JwtAuthenticationToken) SecurityContextHolder.getContext().getAuthentication(); Jwt jwt = (Jwt) authenticationToken.getCredentials(); String email = (String) jwt.getClaims().get("email"); username = (String) jwt.getClaims().get("preferred_username"); thisLog.debug("DEBUG getUsernameFromAuthentication() - Jwt = email=" + email + " username=" + username); } else if (currentPrincipal instanceof DefaultOAuth2AuthenticatedPrincipal) { DefaultOAuth2AuthenticatedPrincipal thisPrincipal = (DefaultOAuth2AuthenticatedPrincipal) currentPrincipal; thisLog.debug("DEBUG getUsernameFromAuthentication() - DefaultOAuth2AuthenticatedPrincipal = " + thisPrincipal.toString()); username = thisPrincipal.getName(); thisLog.debug("getUsernameFromAuthentication() - username="+username); } else { thisLog.error("getUsernameFromAuthentication() -- other or null object type from getPrincipal()"); username = "NOTEXTRACTED"; } return username; } } // end of class
With that class available, a controller method for an endpoint can fetch the username from the request as shown below.
//---------------------------------------------------------------------------------------- // projectList - returns an array of all project objects in the database // URI = /context/servletPath/projects/ // METHOD = GET //---------------------------------------------------------------------------------------- @PreAuthorize("hasRole('ROLE_mdhlabsfull')") @RequestMapping(value="/projects", method=RequestMethod.GET, produces=MediaType.APPLICATION_JSON) public @ResponseBody ListprojectList() throws SQLException, Exception { Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); String username = mdhlabsSecurityUtilities.getUsernameFromAuthentication(authentication); thisLog.info("QUERY action=projectList method=get username=" + username); List listOfProjects = projectsDAO.retrieveProjects(); thisLog.info("REPLY action=projectList method=get username=" + username); return listOfProjects; }
Validating Resource Server Startup
After altering the original web service build with new dependencies in pom.xml
, new configuration in application.properties
and adding dedicated classes related to token validation / transformation, the service is ready for testing. The original web service core still requires MySQL / MariaDB related configuration to be passed via environment variables set with these commands.
export DOCKENV_MYSQL_HOST='192.168.99.10'
export DOCKENV_MYSQL_PORT='3306'
export DOCKENV_MYSQL_USERID='dependsapp'
export DOCKENV_MYSQL_PASSWORD='weakpassword'
After compiling and packaging the service via mvn package
, the service can be started by entering
java -jar ./target/dependsresource.jar
If everything is running and configured correctly, the screen logs will stop with the following message until actual test traffic is sent using commands in the floowing section.
2022-08-23 16:27:47,225 INFO com.mdhlabs.depends.DependsConfiguration - Started DependsConfiguration in 4.293 seconds (JVM running for 4.935)
A few milliseconds prior to that, a message similar to this will confirm the configured SecurityChainFilter and reference
BearerTokenAuthenticationFilter
as shown.2022-08-23 16:27:46,851 INFO org.springframework.security.web.DefaultSecurityFilterChain - Will secure any request with [org.springframework.security.web.session.DisableEncodeUrlFilter@4c27d39d, org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter@40ee0a22, org.springframework.security.web.context.SecurityContextPersistenceFilter@1bcf67e8, org.springframework.security.web.header.HeaderWriterFilter@7f02251, org.springframework.security.web.authentication.logout.LogoutFilter@878537d, org.springframework.security.oauth2.server.resource.web.BearerTokenAuthenticationFilter@677b8e13, org.springframework.security.web.savedrequest.RequestCacheAwareFilter@53692008, org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter@7b2a3ff8, org.springframework.security.web.authentication.AnonymousAuthenticationFilter@7bde1f3a, org.springframework.security.web.session.SessionManagementFilter@4d8126f, org.springframework.security.web.access.ExceptionTranslationFilter@4c98a6d5, org.springframework.security.web.access.intercept.AuthorizationFilter@30331109]
Prior to that, a series of log messages generated by the custom classes will confirm the loading of configuration settings required for token validation and transformation from external to internal values.
2022-08-23 16:27:46,534 DEBUG com.mdhlabs.depends.SecurityConfigurationSFC$$EnhancerBySpringCGLIB$$728df514 - getAuthenticationConverter() - instantiating mdhlabsAuthenticationConverter object
2022-08-23 16:27:46,536 INFO com.mdhlabs.oauthsecurity.mdhlabsJwtAuthenticationConverter - mdhlabsJwtAuthenticationConverter() -- INITIALIZATION - reading config properties from classpath application.properties
2022-08-23 16:27:46,537 INFO com.mdhlabs.oauthsecurity.mdhlabsJwtAuthenticationConverter - mdhlabsAuthenticationConverter() - provider2role = {mdhlabs={mdhlabsgetonly=customer, mdhlabsfull=engineer}, providerx={providerxbasic=customer, providerxadvanced=admin, mdhlabsuser=customer}}
2022-08-23 16:27:46,537 INFO com.mdhlabs.oauthsecurity.mdhlabsJwtAuthenticationConverter - mdhlabsAuthenticationConverter() - provider2path = {mdhlabs=roles, providerx=roles}
2022-08-23 16:27:46,537 INFO com.mdhlabs.oauthsecurity.mdhlabsJwtAuthenticationConverter - mdhlabsAuthenticationConverter() - issuer2provider = {http://192.168.99.10:8011/realms/mdhlabs=mdhlabs, http://192.168.99.10:8011/realms/providerx=providerx}
2022-08-23 16:27:46,537 INFO com.mdhlabs.oauthsecurity.mdhlabsJwtAuthenticationConverter - mdhlabsAuthenticationConverter() - provider2username = {mdhlabs=preferred_username, providerx=preferred_username}
2022-08-23 16:27:46,537 INFO com.mdhlabs.oauthsecurity.mdhlabsJwtAuthenticationConverter - mdhlabsAuthenticationConverter() - provider2claimmap = {}
2022-08-23 16:27:46,537 INFO com.mdhlabs.oauthsecurity.mdhlabsJwtAuthenticationConverter - mdhlabsAuthenticationConverter() - provider2jwkseturi= {mdhlabs=http://192.168.99.10:8011/realms/mdhlabs/protocol/openid-connect/certs, providerx=http://192.168.99.10:8011/realms/providerx/protocol/openid-connect/certs}
2022-08-23 16:27:46,539 DEBUG com.mdhlabs.oauthsecurity.mdhlabsJwtAuthenticationConverter - mdhlabsAuthenticationConverter() - fetching provider=mdhlabs jwkseturi=http://192.168.99.10:8011/realms/mdhlabs/protocol/openid-connect/certs
2022-08-23 16:27:46,570 DEBUG com.mdhlabs.oauthsecurity.mdhlabsJwtAuthenticationConverter - loadProviderJWKSEndpoints() - provider=mdhlabs jwkset=com.auth0.jwk.UrlJwkProvider@4e558728
2022-08-23 16:27:46,606 DEBUG com.mdhlabs.oauthsecurity.mdhlabsJwtAuthenticationConverter - loadProviderJWKSEndpoints() - stored publickey for kid=7UEx1Zb-WAFET4Z4axXo-ZJi2xJ7rV-SQLLEDitt3to in kid2publickey
2022-08-23 16:27:46,606 DEBUG com.mdhlabs.oauthsecurity.mdhlabsJwtAuthenticationConverter - loadProviderJWKSEndpoints() - stored publickey for kid=CThW2f_Ko7WKAg6Ck2lCD2hhtO9Rfo2Wf1tNrdtzDiA in kid2publickey
2022-08-23 16:27:46,606 DEBUG com.mdhlabs.oauthsecurity.mdhlabsJwtAuthenticationConverter - mdhlabsAuthenticationConverter() - fetching provider=providerx jwkseturi=http://192.168.99.10:8011/realms/providerx/protocol/openid-connect/certs
2022-08-23 16:27:46,607 DEBUG com.mdhlabs.oauthsecurity.mdhlabsJwtAuthenticationConverter - loadProviderJWKSEndpoints() - provider=providerx jwkset=com.auth0.jwk.UrlJwkProvider@3ff57625
2022-08-23 16:27:46,613 DEBUG com.mdhlabs.oauthsecurity.mdhlabsJwtAuthenticationConverter - loadProviderJWKSEndpoints() - stored publickey for kid=sSZjvijnbeafQYeII1E5qME8r_DrWSh1liZC8vukeUU in kid2publickey
2022-08-23 16:27:46,613 DEBUG com.mdhlabs.oauthsecurity.mdhlabsJwtAuthenticationConverter - loadProviderJWKSEndpoints() - stored publickey for kid=QE6-c4NkvcLGKTlSTU11JiC5Ap36SDmzPVC0PvDvsDY in kid2publickey
2022-08-23 16:27:46,613 INFO com.mdhlabs.oauthsecurity.mdhlabsJwtAuthenticationConverter - mdhlabsAuthenticationConverter() - mdhlabsAuthenticationConverter() -- initialization completed
2022-08-23 16:27:46,826 INFO com.mdhlabs.depends.SecurityConfigurationSFC$$EnhancerBySpringCGLIB$$728df514 - getAuthenticationFailureHandler() - instantiating mdhlabsAuthenticationFailureHandler
2022-08-23 16:27:46,826 DEBUG com.mdhlabs.depends.SecurityConfigurationSFC$$EnhancerBySpringCGLIB$$728df514 - getAuthorizationFailureHandler() - instantiating mdhlabsAuthorizationFailureHandler
2022-08-23 16:27:46,827 DEBUG com.mdhlabs.depends.SecurityConfigurationSFC$$EnhancerBySpringCGLIB$$728df514 - mdhlabsAuthenticationResolver() - returning reference to mdhlabsAuthenticationManager for token processing
2022-08-23 16:27:46,828 DEBUG com.mdhlabs.depends.SecurityConfigurationSFC$$EnhancerBySpringCGLIB$$728df514 - getBearerTokenResolver() - instantiating mdhlabsBearerTokenResolver
Earlier in the startup logs, the following entries are generated by the DAO built to perform the underlying database queries to confirm the required JDBC configuration values were parsed and used to connect to the database.
2022-08-23 16:27:46,336 INFO com.mdhlabs.depends.dao.ProjectsDAO - ProjectsDAO constructor
2022-08-23 16:27:46,342 INFO com.mdhlabs.depends.dao.ProjectsDAO - DOCKENV_MARIADB_HOST=192.168.99.10 DOCKENV_MARIADB_PORT=3306 DOCKENV_MARIADB_USERID=dependsapp DOCKENV_MARIADB_PASSWORD=badpassword
Command Line Testing Resource Server Functionality
Before diving into details, it is wise to summarize the different tests that should be executed against any implementation of a Resource Server. It is easier to find and correct any flaws from the command line during development than after a service has been exposed to upstream developers creating clients of the Resource Server.
Test ID | Scenario | Expected Behavior |
---|---|---|
1 | no token submitted | Return response with status=401 and "WWW-Authenticate: Bearer" header |
2 | Valid token, correct role submitted | JSON payload of response data |
3 | Valid token, insufficient role | Return response with status=403 and "WWW-Authenticate: Bearer" header |
4 | Expired token submitted | Return response with status=401 and "WWW-Authenticate: Bearer" header |
5 | Corrupt / invalid token submitted | Return response with status=401 and "WWW-Authenticate: Bearer" header |
Test - No Token Submitted
With the dependsresource.jar
running in one window, executing the commands below which do not submit any access token header should return 401 responses that include "WWW-Authenticate: Bearer" as a response header as a trigger to a client they need to initiate creation of a token.
[mdh@fedora1 ~/gitwork/oauthdependsresource]$curl --verbose -H "Content-type: application/json" -X GET http://127.0.0.1:8080/depends/api/project/34
{"timestamp":"2021-03-21T23:41:18.574+0000","status":401,"error":"Unauthorized","message":"Unauthorized","path":"/depends/api/project/34"} [mdh@fedora1 ~/gitwork/oauthdependsresource]$ [mdh@fedora1 ~/gitwork/oauthdependsresource]$ [mdh@fedora1 ~/gitwork/oauthdependsresource]$ [mdh@fedora1 ~/gitwork/oauthdependsresource]$curl --verbose -H "Content-type: application/json" -X GET http://127.0.0.1:8080/depends/api/projects
{"timestamp":"2021-03-21T23:41:29.157+0000","status":401,"error":"Unauthorized","message":"Unauthorized","path":"/depends/api/projects"} [mdh@fedora1 ~/gitwork/oauthdependsresource]$ [mdh@fedora1 ~/gitwork/oauthdependsresource]$ [mdh@fedora1 ~/gitwork/oauthdependsresource]$
Test - Fetching Valid Token from Authorization Server
Recapping tests performed in the Authorization Server section, we first need to obtain a valid access token from the Authorization Server. For these test scenarios, our "client" will use the client credentials flow with Keycloak to obtain a token which will then be submitted in requests to our Resource Server web service. The first command below chains together multiple shell commmands that:
- submits a client-credentials request to Keycloak for a token and stuffs the result into a variable $REPLY
- parses $REPLY once to extract the access_token string out of the response and save it as variable $ACCESS_TOKEN
- parses $REPLY a second time to extract any refresh_token string out of the response and save it as $REFRESH_TOKEN
The $ACCESS_TOKEN variable can be displayed and / or included in subsequent commands when the token is submitted in a request header as shown in the subsequent examples.
Test - Valid Token, Correct Role Submitted
For the DependsClientBasic, a call to /depends/api/project/35
should return a project JSON object since it is entitled to run that endpoint.
mdh@fedora1:~/gitwork $REPLY=$( curl -d "client_id=DependsClientBasic" -d "grant_type=client_credentials" -d "client_secret=WePONVSOYbnWDRbg6Uexrgpmc6cYKEHV" "http://192.168.99.10:8011/realms/mdhlabs/protocol/openid-connect/token"); ACCESS_TOKEN=$(echo $REPLY | jq -r '.access_token');REFRESH_TOKEN=$(echo $REPLY | jq -r '.refresh_token')
% Total % Received % Xferd Average Speed Time Time Time Current Dload Upload Total Spent Left Speed 100 1775 100 1670 100 105 59066 3713 --:--:-- --:--:-- --:--:-- 63392 mdh@fedora1:~/gitwork $ mdh@fedora1:~/gitwork $curl -H "Content-type: application/json" -H "Authorization: Bearer $ACCESS_TOKEN" -X GET http://192.168.99.10:8080/depends/api/project/35
{"project_id":35,"projectstatus_id":0,"clientbusunit_id":2,"clientbusdept_id":2,"factorybusunit_id":6,"factorybusdept_id":7,"projectname":"Abuse Unification to CATS","shortdescription":"Unified integration to WFX","longdescription":"Longer desc here","hascapitalspend":"Y","hasexpensespend":"Y","capitalledger":"","expenseledger":"","clientpriority":1,"deliverypriority":1,"restricttodept":"N","restricttomembers":"N","createdatetime":"2018-05-20 20:51:22","updatedatetime":"0000-00-00 00:00:00"}mdh@fedora1:~/gitwork $ mdh@fedora1:~/gitwork $
Test - Valid Token, Insufficient Role Submitted
The first command below chains together shell commands to capture the response from the Authorization Server for a client_credentials request which will return an access_token which is then extracted into $ACCESS_TOKEN as a shell variable. At that point, the second command sends a GET injecting the token as an Authorization header to the Resource Server endpoint.
For the DependsClientBasic, a call to /depends/api/projects
should return a 403 error since that endpoint requires the engineer
role and the external mdhlabsgetonly role for DependsClientBasic only maps to the internal role customer
.
mdh@fedora1:~/gitwork/oauthdependsresource $REPLY=$( curl -d "client_id=DependsClientBasic" -d "grant_type=client_credentials" -d "client_secret=WePONVSOYbnWDRbg6Uexrgpmc6cYKEHV" "http://192.168.99.10:8011/realms/mdhlabs/protocol/openid-connect/token"); ACCESS_TOKEN=$(echo $REPLY | jq -r '.access_token');REFRESH_TOKEN=$(echo $REPLY | jq -r '.refresh_token')
% Total % Received % Xferd Average Speed Time Time Time Current Dload Upload Total Spent Left Speed 100 1521 100 1416 100 105 96906 7185 --:--:-- --:--:-- --:--:-- 106k mdh@fedora1:~/gitwork/oauthdependsresource $ mdh@fedora1:~/gitwork/oauthdependsresource $ mdh@fedora1:~/gitwork/oauthdependsresource $curl -H "Content-type: application/json" -H "Authorization: Bearer $ACCESS_TOKEN" -X GET http://192.168.99.10:8080/depends/api/projects
{"exception":"org.springframework.security.access.AccessDeniedException","message":"Access is denied","timestamp":1660504041558} mdh@fedora1:~/gitwork/oauthdependsresource $
For the DependsClientFull client, a call to /depends/api/projects
should succeed and return a list of all projects since it has the external role mdhlabsfull
in Keycloak which maps to engineer
within the web service.
mdh@fedora1:~/gitwork/oauthdependsresource $REPLY=$( curl -d "client_id=DependsClientFull" -d "grant_type=client_credentials" -d "client_secret=ediQAgqkh4fwaOP5t5vaHLElj29fskSz" "http://192.168.99.10:8011/realms/mdhlabs/protocol/openid-connect/token"); ACCESS_TOKEN=$(echo $REPLY | jq -r '.access_token');REFRESH_TOKEN=$(echo $REPLY | jq -r '.refresh_token')
% Total % Received % Xferd Average Speed Time Time Time Current Dload Upload Total Spent Left Speed 100 1521 100 1416 100 105 96906 7185 --:--:-- --:--:-- --:--:-- 106k mdh@fedora1:~/gitwork/oauthdependsresource $ mdh@fedora1:~/gitwork/oauthdependsresource $ mdh@fedora1:~/gitwork/oauthdependsresource $curl -H "Content-type: application/json" -H "Authorization: Bearer $ACCESS_TOKEN" -X GET http://192.168.99.10:8080/depends/api/projects
mdh@fedora1:~/gitwork/oauthdependsresource $ curl -H "Content-type: application/json" -H "Authorization: Bearer $ACCESS_TOKEN" -X GET http://192.168.99.10:8080/depends/api/projects [{"project_id":1,"projectstatus_id":0,"clientbusunit_id":2,"clientbusdept_id":2,"factorybusunit_id":6,"factorybusdept_id":7,"projectname":"SoloCustomers","shortdescription":"Build services for CUSTOMER object","longdescription":"Longer desc here","hascapitalspend":"Y","hasexpensespend":"Y","capitalledger":"","expenseledger":"","clientpriority":1,"deliverypriority":1,"restricttodept":"N","restricttomembers":"N","createdatetime":"2018-05-20 20:51:22","updatedatetime":"0000-00-00 00:00:00"}, {"project_id":2,"projectstatus_id":0,"clientbusunit_id":2,"clientbusdept_id":2,"factorybusunit_id":6,"factorybusdept_id":7,"projectname":"SoloAccounts","shortdescription":"Build model for ACCOUNT object","longdescription":"Longer desc here","hascapitalspend":"Y","hasexpensespend":"Y","capitalledger":"","expenseledger":"","clientpriority":1,"deliverypriority":1,"restricttodept":"N","restricttomembers":"N","createdatetime":"2018-05-20 20:51:22","updatedatetime":"0000-00-00 00:00:00"}, {"project_id":3,"projectstatus_id":0,"clientbusunit_id":2,"clientbusdept_id":2,"factorybusunit_id":6,"factorybusdept_id":7,"projectname":"SoloWallets","shortdescription":"Build model for WALLET object","longdescription":"Longer desc here","hascapitalspend":"Y","hasexpensespend":"Y","capitalledger":"","expenseledger":"","clientpriority":1,"deliverypriority":1,"restricttodept":"N","restricttomembers":"N","createdatetime":"2018-05-20 20:51:22","updatedatetime":"0000-00-00 00:00:00"}, NUMEROUS JSON ENTRIES OMITTED FOR BREVITY.... {"project_id":38,"projectstatus_id":0,"clientbusunit_id":2,"clientbusdept_id":2,"factorybusunit_id":6,"factorybusdept_id":7,"projectname":"Members Only Test Project","shortdescription":"Only show to members of project","longdescription":"Longer desc here","hascapitalspend":"Y","hasexpensespend":"Y","capitalledger":"","expenseledger":"","clientpriority":1,"deliverypriority":1,"restricttodept":"N","restricttomembers":"Y","createdatetime":"2018-05-20 20:51:22","updatedatetime":"0000-00-00 00:00:00"}]mdh@fedora1:~/gitwork/oauthdependsresource $ mdh@fedora1:~/gitwork/oauthdependsresource $ mdh@fedora1:~/gitwork/oauthdependsresource $
Test - Expired Token Submitted
Using the same token previously generated after waiting 300 seconds for the token to expire, submitted a request again should return a 401 error that also includes WWW-Authenticate: Bearer as a header to trigger a client to request a new token.
mdh@fedora1:~/gitwork $ curl --verbose -H "Content-type: application/json" -H "Authorization: Bearer $ACCESS_TOKEN" -X GET http://192.168.99.10:8080/depends/api/project/35
Note: Unnecessary use of -X or --request, GET is already inferred.
* Trying 192.168.99.10:8080...
* Connected to 192.168.99.10 (192.168.99.10) port 8080 (#0)
> GET /depends/api/projects HTTP/1.1
> Host: 192.168.99.10:8080
> User-Agent: curl/7.82.0
> Accept: */*
> Content-type: application/json
> Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJDVGhXMmZfS283V0tBZzZDazJsQ0QyaGh0TzlSZm8yV2YxdE5yZHR6RGlBIn0.eyJleHAiOjE2NjEzMDA0OTIsImlhdCI6MTY2MTMwMDM3MiwianRpIjoiZGU3MTRlMTMtOGE5ZC00MGM5LWI4NTEtMTMyMzI2NGY4Yzc2IiwiaXNzIjoiaHR0cDovLzE5Mi4xNjguOTkuMTA6ODAxMS9yZWFsbXMvbWRobGFicyIsImF1ZCI6ImFjY291bnQiLCJzdWIiOiI5MjNjM2UxNi01MDMyLTRkZjctYTg1MS1jNjBmYmQ1ZmZjODIiLCJ0eXAiOiJCZWFyZXIiLCJhenAiOiJEZXBlbmRzQ2xpZW50RnVsbCIsImFjciI6IjEiLCJyZXNvdXJjZV9hY2Nlc3MiOnsiYWNjb3VudCI6eyJyb2xlcyI6WyJtYW5hZ2UtYWNjb3VudCIsIm1hbmFnZS1hY2NvdW50LWxpbmtzIiwidmlldy1wcm9maWxlIl19fSwic2NvcGUiOiJlbWFpbCBwcm9maWxlIiwiY2xpZW50SWQiOiJEZXBlbmRzQ2xpZW50RnVsbCIsImNsaWVudEhvc3QiOiIxOTIuMTY4Ljk5LjEwIiwiZW1haWxfdmVyaWZpZWQiOmZhbHNlLCJyb2xlcyI6WyJvZmZsaW5lX2FjY2VzcyIsInVtYV9hdXRob3JpemF0aW9uIiwiZGVmYXVsdC1yb2xlcy1tZGhsYWJzIiwibWRobGFic2Z1bGwiLCJvZmZsaW5lX2FjY2VzcyIsInVtYV9hdXRob3JpemF0aW9uIiwiZGVmYXVsdC1yb2xlcy1tZGhsYWJzIiwibWRobGFic2Z1bGwiXSwicHJlZmVycmVkX3VzZXJuYW1lIjoic2VydmljZS1hY2NvdW50LWRlcGVuZHNjbGllbnRmdWxsIiwiY2xpZW50QWRkcmVzcyI6IjE5Mi4xNjguOTkuMTAifQ.BRGyixoxhzu1N4M9VBQH7bghn-sBjYk-YvKUmzCQ55NK9FP-tnvEn6NQaNs8q02ZWJJTQrd_6DasV8EfWDDsHoz5UmQrU0GoNgpgN_z-blt_oDqPX7hHXsW7gqpLLhcQneeC1fKm187Y55VoBnwjC8SfEAh6aZBNA_nQ9mCZeL1JtrIAZxghlZCywnKDGR86XA07mkxH9oT3b8l6UkKJ5M37MQHZ6V59ooUN-lDNABRoohUdKZXQ5dFz1jjhDPJ7WzabJ-J5dVo5W_uVPskTUuIGbGU0or7bGEI_8yuM49jfJ3S-AKXXn0VXKm8uYej4nReXH-n7D8oZTj8bQNsrWw
>
* Mark bundle as not supporting multiuse
< HTTP/1.1 401
< X-Content-Type-Options: nosniff
< X-XSS-Protection: 1; mode=block
< Cache-Control: no-cache, no-store, max-age=0, must-revalidate
< Pragma: no-cache
< Expires: 0
< X-Frame-Options: DENY
< Content-Length: 138
< Date: Wed, 24 Aug 2022 00:27:13 GMT
<
{"exception":"org.springframework.security.authentication.BadCredentialsException","message":"invalidExpired","timestamp":1661300833291}
* Connection #0 to host 192.168.99.10 left intact
mdh@fedora1:~/gitwork $
![]() |
It is useful to conduct an additional test to send the same request repeatedly and validate how tightly token expirations are enforced. In a prior incarnation of this service which relied on Spring libraries for some of the token validation, testing confirmed tokens were honored up to 60 seconds past their nominal expiration time. A token returned to be valid for 120 seconds was accepted up to 180 seconds. A token generated with a 180 second interval began encountering rejections at 240 seconds. As crafted with this custom JwtValidationUtilities class, token expirations are STRICTLY enforced. Any request arriving after the assigned expiration parameter is rejected with the above exception returned to the client in a 401 response. |
Test - Corrupt / Invalid Token Submitted
To test handling of a corrupt or invalid token, the $ACCESS_TOKEN value can be echo'ed to the screen then altered (a single character difference will invalidate it) then set back to the $ACCESS_TOKEN variable. At that point, re-using the same curl command now using the altered $ACCESS_TOKEN will test handling of a corrupt token. In the example below, a previously valid token is altered to set the last three characters to xxx, thus invalidating its cryptographic signature. The Resource Server should return a 401 error and also include WWW-Authenticate: Bearer as a header to trigger a client to request a new token.
mdh@fedora1:~/gitwork/oauthdependsresource $REPLY=$( curl -d "client_id=DependsClientFull" -d "grant_type=client_credentials" -d "client_secret=ediQAgqkh4fwaOP5t5vaHLElj29fskSz" "http://192.168.99.10:8011/realms/mdhlabs/protocol/openid-connect/token"); ACCESS_TOKEN=$(echo $REPLY | jq -r '.access_token');REFRESH_TOKEN=$(echo $REPLY | jq -r '.refresh_token')
% Total % Received % Xferd Average Speed Time Time Time Current Dload Upload Total Spent Left Speed 100 1612 100 1508 100 104 101k 7157 --:--:-- --:--:-- --:--:-- 112k mdh@fedora1:~/gitwork/oauthdependsresource $echo $ACCESS_TOKEN
eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJDVGhXMmZfS283V0tBZzZDazJsQ0QyaGh0TzlSZm8yV2YxdE5yZHR6RGlBIn0.eyJleHAiOjE2NjEzMDIwMjAsImlhdCI6MTY2MTMwMTkwMCwianRpIjoiZDViYWNjNDgtYzNhOC00MGY5LTkxMzUtODczZWZiYzRhNDkwIiwiaXNzIjoiaHR0cDovLzE5Mi4xNjguOTkuMTA6ODAxMS9yZWFsbXMvbWRobGFicyIsImF1ZCI6ImFjY291bnQiLCJzdWIiOiI5MjNjM2UxNi01MDMyLTRkZjctYTg1MS1jNjBmYmQ1ZmZjODIiLCJ0eXAiOiJCZWFyZXIiLCJhenAiOiJEZXBlbmRzQ2xpZW50RnVsbCIsImFjciI6IjEiLCJyZXNvdXJjZV9hY2Nlc3MiOnsiYWNjb3VudCI6eyJyb2xlcyI6WyJtYW5hZ2UtYWNjb3VudCIsIm1hbmFnZS1hY2NvdW50LWxpbmtzIiwidmlldy1wcm9maWxlIl19fSwic2NvcGUiOiJlbWFpbCBwcm9maWxlIiwiY2xpZW50SWQiOiJEZXBlbmRzQ2xpZW50RnVsbCIsImNsaWVudEhvc3QiOiIxOTIuMTY4Ljk5LjEwIiwiZW1haWxfdmVyaWZpZWQiOmZhbHNlLCJyb2xlcyI6WyJvZmZsaW5lX2FjY2VzcyIsInVtYV9hdXRob3JpemF0aW9uIiwiZGVmYXVsdC1yb2xlcy1tZGhsYWJzIiwibWRobGFic2Z1bGwiLCJvZmZsaW5lX2FjY2VzcyIsInVtYV9hdXRob3JpemF0aW9uIiwiZGVmYXVsdC1yb2xlcy1tZGhsYWJzIiwibWRobGFic2Z1bGwiXSwicHJlZmVycmVkX3VzZXJuYW1lIjoic2VydmljZS1hY2NvdW50LWRlcGVuZHNjbGllbnRmdWxsIiwiY2xpZW50QWRkcmVzcyI6IjE5Mi4xNjguOTkuMTAifQ.iD90BPphbP8cYXfnNQLgZ2BVhXkcMAr5cv01_-DBqceBJm5JZ54BG7xHFKGKhb4Q3YKe_eS9bd6mdTZY2SXGvthYKy-CNnk92O4siLBIq3W5cVJO2AfHEGOmdyIxE55D49U9qwKCipKGpHVIzJdcP5r5n36PC60HR_aySRW2W-49RzkfAO0mbwAFw_s4fTWT6aE7-oxWp7eG4ToJuJjzlAuRzbdeMIXFIpifJhfyS_UkMGmrhKXP0ScbmTl8uDJQe3Ks5-EvwoQRRaXAUPkqLYWKGBbOyDbhDq_aIJo9GE98zwm6WHaRwVjrKEdyyyD2HoNr5i8_xBhSl1noI4drSQ mdh@fedora1:~/gitwork/oauthdependsresource $export ACCESS_TOKEN=eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJDVGhXMmZfS283V0tBZzZDazJsQ0QyaGh0TzlSZm8yV2YxdE5yZHR6RGlBIn0.eyJleHAiOjE2NjEzMDIwMjAsImlhdCI6MTY2MTMwMTkwMCwianRpIjoiZDViYWNjNDgtYzNhOC00MGY5LTkxMzUtODczZWZiYzRhNDkwIiwiaXNzIjoiaHR0cDovLzE5Mi4xNjguOTkuMTA6ODAxMS9yZWFsbXMvbWRobGFicyIsImF1ZCI6ImFjY291bnQiLCJzdWIiOiI5MjNjM2UxNi01MDMyLTRkZjctYTg1MS1jNjBmYmQ1ZmZjODIiLCJ0eXAiOiJCZWFyZXIiLCJhenAiOiJEZXBlbmRzQ2xpZW50RnVsbCIsImFjciI6IjEiLCJyZXNvdXJjZV9hY2Nlc3MiOnsiYWNjb3VudCI6eyJyb2xlcyI6WyJtYW5hZ2UtYWNjb3VudCIsIm1hbmFnZS1hY2NvdW50LWxpbmtzIiwidmlldy1wcm9maWxlIl19fSwic2NvcGUiOiJlbWFpbCBwcm9maWxlIiwiY2xpZW50SWQiOiJEZXBlbmRzQ2xpZW50RnVsbCIsImNsaWVudEhvc3QiOiIxOTIuMTY4Ljk5LjEwIiwiZW1haWxfdmVyaWZpZWQiOmZhbHNlLCJyb2xlcyI6WyJvZmZsaW5lX2FjY2VzcyIsInVtYV9hdXRob3JpemF0aW9uIiwiZGVmYXVsdC1yb2xlcy1tZGhsYWJzIiwibWRobGFic2Z1bGwiLCJvZmZsaW5lX2FjY2VzcyIsInVtYV9hdXRob3JpemF0aW9uIiwiZGVmYXVsdC1yb2xlcy1tZGhsYWJzIiwibWRobGFic2Z1bGwiXSwicHJlZmVycmVkX3VzZXJuYW1lIjoic2VydmljZS1hY2NvdW50LWRlcGVuZHNjbGllbnRmdWxsIiwiY2xpZW50QWRkcmVzcyI6IjE5Mi4xNjguOTkuMTAifQ.iD90BPphbP8cYXfnNQLgZ2BVhXkcMAr5cv01_-DBqceBJm5JZ54BG7xHFKGKhb4Q3YKe_eS9bd6mdTZY2SXGvthYKy-CNnk92O4siLBIq3W5cVJO2AfHEGOmdyIxE55D49U9qwKCipKGpHVIzJdcP5r5n36PC60HR_aySRW2W-49RzkfAO0mbwAFw_s4fTWT6aE7-oxWp7eG4ToJuJjzlAuRzbdeMIXFIpifJhfyS_UkMGmrhKXP0ScbmTl8uDJQe3Ks5-EvwoQRRaXAUPkqLYWKGBbOyDbhDq_aIJo9GE98zwm6WHaRwVjrKEdyyyD2HoNr5i8_xBhSl1noI4drSz
mdh@fedora1:~/gitwork/oauthdependsresource $ mdh@fedora1:~/gitwork/oauthdependsresource $curl --verbose -H "Content-type: application/json" -H "Authorization: Bearer $ACCESS_TOKEN" -X GET http://192.168.99.10:8080/depends/api/project/32
Note: Unnecessary use of -X or --request, GET is already inferred. * Trying 192.168.99.10:8080... * Connected to 192.168.99.10 (192.168.99.10) port 8080 (#0) > GET /depends/api/project/32 HTTP/1.1 > Host: 192.168.99.10:8080 > User-Agent: curl/7.82.0 > Accept: */* > Content-type: application/json > Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJDVGhXMmZfS283V0tBZzZDazJsQ0QyaGh0TzlSZm8yV2YxdE5yZHR6RGlBIn0.eyJleHAiOjE2NjEzMDIwMjAsImlhdCI6MTY2MTMwMTkwMCwianRpIjoiZDViYWNjNDgtYzNhOC00MGY5LTkxMzUtODczZWZiYzRhNDkwIiwiaXNzIjoiaHR0cDovLzE5Mi4xNjguOTkuMTA6ODAxMS9yZWFsbXMvbWRobGFicyIsImF1ZCI6ImFjY291bnQiLCJzdWIiOiI5MjNjM2UxNi01MDMyLTRkZjctYTg1MS1jNjBmYmQ1ZmZjODIiLCJ0eXAiOiJCZWFyZXIiLCJhenAiOiJEZXBlbmRzQ2xpZW50RnVsbCIsImFjciI6IjEiLCJyZXNvdXJjZV9hY2Nlc3MiOnsiYWNjb3VudCI6eyJyb2xlcyI6WyJtYW5hZ2UtYWNjb3VudCIsIm1hbmFnZS1hY2NvdW50LWxpbmtzIiwidmlldy1wcm9maWxlIl19fSwic2NvcGUiOiJlbWFpbCBwcm9maWxlIiwiY2xpZW50SWQiOiJEZXBlbmRzQ2xpZW50RnVsbCIsImNsaWVudEhvc3QiOiIxOTIuMTY4Ljk5LjEwIiwiZW1haWxfdmVyaWZpZWQiOmZhbHNlLCJyb2xlcyI6WyJvZmZsaW5lX2FjY2VzcyIsInVtYV9hdXRob3JpemF0aW9uIiwiZGVmYXVsdC1yb2xlcy1tZGhsYWJzIiwibWRobGFic2Z1bGwiLCJvZmZsaW5lX2FjY2VzcyIsInVtYV9hdXRob3JpemF0aW9uIiwiZGVmYXVsdC1yb2xlcy1tZGhsYWJzIiwibWRobGFic2Z1bGwiXSwicHJlZmVycmVkX3VzZXJuYW1lIjoic2VydmljZS1hY2NvdW50LWRlcGVuZHNjbGllbnRmdWxsIiwiY2xpZW50QWRkcmVzcyI6IjE5Mi4xNjguOTkuMTAifQ.iD90BPphbP8cYXfnNQLgZ2BVhXkcMAr5cv01_-DBqceBJm5JZ54BG7xHFKGKhb4Q3YKe_eS9bd6mdTZY2SXGvthYKy-CNnk92O4siLBIq3W5cVJO2AfHEGOmdyIxE55D49U9qwKCipKGpHVIzJdcP5r5n36PC60HR_aySRW2W-49RzkfAO0mbwAFw_s4fTWT6aE7-oxWp7eG4ToJuJjzlAuRzbdeMIXFIpifJhfyS_UkMGmrhKXP0ScbmTl8uDJQe3Ks5-EvwoQRRaXAUPkqLYWKGBbOyDbhDq_aIJo9GE98zwm6WHaRwVjrKEdyyyD2HoNr5i8_xBhSl1noI4drSz > * Mark bundle as not supporting multiuse < HTTP/1.1 401 < X-Content-Type-Options: nosniff < X-XSS-Protection: 1; mode=block < Cache-Control: no-cache, no-store, max-age=0, must-revalidate < Pragma: no-cache < Expires: 0 < X-Frame-Options: DENY < Content-Length: 148 < Date: Wed, 24 Aug 2022 00:47:14 GMT < {"exception":"org.springframework.security.authentication.BadCredentialsException","message":"invalidSignatureMismatch","timestamp":1661302034916} * Connection #0 to host 192.168.99.10 left intact mdh@fedora1:~/gitwork/oauthdependsresource $