Repository: SAP/cloud-security-xsuaa-integration
Pull request #134 (2019-09-06 13:11:46)

Add a simple Base64 JWT decoder

CLAassistant: [![CLA assistant check](https://cla-assistant.io/pull/badge/not_signed)](https://cla-assistant.io/SAP/cloud-security-xsuaa-integration?pullRequest=134) <br/>Thank you for your submission, we really appreciate it. Like many open source projects, we ask that you sign our [Contributor License Agreement](https://cla-assistant.io/SAP/cloud-security-xsuaa-integration?pullRequest=134) before we can accept your contribution.<br/><hr/>**Alexander Biskop** seems not to be a GitHub user. You need a GitHub account to be able to sign the CLA. If you have already a GitHub account, please [add the email address used for this commit to your account](https://help.github.com/articles/why-are-my-commits-linked-to-the-wrong-user/#commits-are-not-linked-to-any-user).<br/><sub>You have signed the CLA already but the status is still pending? Let us [recheck](https://cla-assistant.io/check/SAP/cloud-security-xsuaa-integration?pullRequest=134) it.</sub>
abiskop: @nenaraab - as discussed per e-mail

Patch:
diff --git a/README.md b/README.md
index dc67beda..cda42fe6 100644
--- a/README.md
+++ b/README.md
@@ -23,16 +23,6 @@ The SAP Java Buildpack integrates token validation into the tomcat server. Appli
 ### Sample
 See [sap-java-builpack-api-uage](samples/sap-java-buildpack-api-usage) for an example.
 
-## Token Exchange for Java applications
-Applications requiring access tokens (Jwt) use the Token Flows API defined in [token-client](./token-client) to obtain Jwt tokens for their clients (applications) or for their users.
-
-### Requirements
-- Java 8
-- maven 3.3.9 or later
-
-### Sample
-See [spring-security-xsuaa-usage](samples/spring-security-xsuaa-usage) for an example.
-
 ## Token Validation for Java Spring Boot web applications
 Spring Boot provides OAuth resource servers. Application developers requiring authentication and authorization information in their application use the libraries defined in [spring-xsuaa](./spring-xsuaa) to obtain token information like user name and scopes.
 
diff --git a/pom.xml b/pom.xml
index 8e6a49b9..985035ae 100644
--- a/pom.xml
+++ b/pom.xml
@@ -176,7 +176,7 @@
 					<plugin>
 						<groupId>org.apache.maven.plugins</groupId>
 						<artifactId>maven-gpg-plugin</artifactId>
-						<version>1.6</version>
+						<version>1.5</version>
 						<executions>
 							<execution>
 								<id>sign-artifacts</id>
@@ -184,14 +184,6 @@
 								<goals>
 									<goal>sign</goal>
 								</goals>
-								<configuration>
-									<!-- GPG keyname var set in the settings.xml maven file -->
-									<keyname>${gpg.keyname}</keyname>
-									<gpgArguments>
-										<arg>--pinentry-mode</arg>
-										<arg>loopback</arg>
-									</gpgArguments>
-								</configuration>
 							</execution>
 						</executions>
 					</plugin>
@@ -199,7 +191,7 @@
 					<plugin>
 						<groupId>org.sonatype.plugins</groupId>
 						<artifactId>nexus-staging-maven-plugin</artifactId>
-						<version>1.6.8</version>
+						<version>1.6.7</version>
 						<extensions>true</extensions>
 						<configuration>
 							<serverId>ossrh</serverId>
diff --git a/spring-xsuaa/pom.xml b/spring-xsuaa/pom.xml
index f4813ad1..dcb8a83c 100644
--- a/spring-xsuaa/pom.xml
+++ b/spring-xsuaa/pom.xml
@@ -59,7 +59,6 @@
 			<groupId>${project.groupId}</groupId>
 			<artifactId>token-client</artifactId>
 			<version>${project.version}</version>
-
 		</dependency>
 		<dependency>
 			<groupId>com.github.ben-manes.caffeine</groupId>
@@ -100,12 +99,6 @@
 			<artifactId>spring-boot-starter-test</artifactId>
 			<version>${spring.boot.version}</version>
 			<scope>test</scope>
-			<exclusions>
-				<exclusion>
-					<groupId>com.vaadin.external.google</groupId>
-					<artifactId>android-json</artifactId>
-				</exclusion>
-			</exclusions>
 		</dependency>
 		<dependency>
 			<groupId>org.springframework.security</groupId>
diff --git a/spring-xsuaa/src/main/java/com/sap/xs2/security/container/SecurityContext.java b/spring-xsuaa/src/main/java/com/sap/xs2/security/container/SecurityContext.java
index 5ce120b1..8cea3351 100644
--- a/spring-xsuaa/src/main/java/com/sap/xs2/security/container/SecurityContext.java
+++ b/spring-xsuaa/src/main/java/com/sap/xs2/security/container/SecurityContext.java
@@ -16,8 +16,7 @@ import org.springframework.security.oauth2.jwt.JwtDecoder;
  * {@code com.sap.xs2.security.container} package. It will be removed with
  * version {@code 2.0.0}
  *
- * @deprecated use {@link SpringSecurityContext} class instead. Will be removed
- *             with version 2.0.
+ * @deprecated use {@link SpringSecurityContext} class instead. Will be removed with version 2.0.
  */
 @Deprecated
 public class SecurityContext {
diff --git a/spring-xsuaa/src/main/java/com/sap/xs2/security/container/UserInfo.java b/spring-xsuaa/src/main/java/com/sap/xs2/security/container/UserInfo.java
index 7e63c825..f1bc8952 100644
--- a/spring-xsuaa/src/main/java/com/sap/xs2/security/container/UserInfo.java
+++ b/spring-xsuaa/src/main/java/com/sap/xs2/security/container/UserInfo.java
@@ -24,7 +24,6 @@ import net.minidev.json.JSONObject;
 /**
  * Class providing access to common user related attributes extracted from the
  * JWT token.
- * 
  * @deprecated will be removed with version 2.0
  */
 @Deprecated
diff --git a/spring-xsuaa/src/main/java/com/sap/xs2/security/container/XSTokenRequestImpl.java b/spring-xsuaa/src/main/java/com/sap/xs2/security/container/XSTokenRequestImpl.java
index 6ca2ea05..486c258e 100644
--- a/spring-xsuaa/src/main/java/com/sap/xs2/security/container/XSTokenRequestImpl.java
+++ b/spring-xsuaa/src/main/java/com/sap/xs2/security/container/XSTokenRequestImpl.java
@@ -18,8 +18,7 @@ import com.sap.xsa.security.container.XSTokenRequest;
 import org.springframework.web.client.RestTemplate;
 
 /**
- * @deprecated in favor of XsuaaTokenFlows API. Will be removed with version
- *             2.0.
+ * @deprecated in favor of XsuaaTokenFlows API. Will be removed with version 2.0.
  */
 @Deprecated
 public class XSTokenRequestImpl implements XSTokenRequest {
diff --git a/token-client/README.md b/token-client/README.md
index db83fb10..afea3e9d 100644
--- a/token-client/README.md
+++ b/token-client/README.md
@@ -5,14 +5,11 @@
 This library serves as slim client for some XSUAA `/oauth/token` token endpoints as specified [here](https://docs.cloudfoundry.org/api/uaa/version/4.30.0/index.html#token). 
 Furthermore it introduces a new API to support the following token flows:
 
-* **User Token Flow**.  
-The idea behind a User Token exchange is to separate service-specific access scopes into separate tokens. For example, if Service A has scopes specific to its functionality and Service B has other scopes, the intention is that there is no single Jwt token that contains all of these scopes.
-* **Client Credentials Flow**.  
-The Client Credentials ([RFC 6749, section 4.4](https://tools.ietf.org/html/rfc6749#section-4.4)) is used by clients to obtain an access token outside of the context of a user. It is used for non interactive applications (a CLI, a batch job, or for service-2-service communication) where the token is issued to the application itself, instead of an end user for accessing resources without principal propagation. 
-* **Refresh Token Flow**.  
-A Refresh Token ([RFC 6749, section 1.5](https://tools.ietf.org/html/rfc6749#section-1.5)) flow allows you to obtain a new access token in case the current one becomes invalid or expires.
+* User Token Flow
+* Client Credentials Flow
+* Refresh Token Flow
 
-> Note: The **Authorization Code Grant Flow** involves the browser and is therefore triggered by an API gateway (e.g. Application Router). The other flows, however, may need to be triggered programmatically, e.g. to exchange one token for another or refresh a token, if it is about to expire. When you create a XSUAA service instance a OAuth client gets created and you receive the client credentials (client id and secret) when you bind your application with the XSUAA service instance. Having that in place you are ready to use the token flows in your Java application.
+The Authorization Code Grant Flow involves the browser and is therefore triggered by an API gateway (e.g. Application Router). The other flows, however, may need to be triggered programmatically, e.g. to exchange one token for another or refresh a token, if it is about to expire. When you create a XSUAA service instance a OAuth client gets created and you receive the client credentials (client id and secret) when you bind your application with the XSUAA service instance. Having that in place you are ready to use the token flows in your Java application.
 
 ## Configuration for Java Applications
 
@@ -73,39 +70,35 @@ RestOperations restOperations = new RestTemplate();
 XsuaaTokenFlows tokenFlows = new XsuaaTokenFlows(restOperations, endpointsProvider, clientCredentials);
 ```
 
-### Client Credentials Token Flow
-Obtain a client credentials token:
+Then, to create a **client credentials token flow** very easily by the following code:
 
 ```java
 OAuth2TokenResponse clientCredentialsToken = tokenFlows.clientCredentialsTokenFlow()
-                                                    .subdomain(jwtToken.getSubdomain()) // this is optional 
                                                     .execute();
 ```
-### Refresh Token Flow
-In case you have a refresh token and want to obtain an access token:
+
+In case you have a refresh_token value and want to refresh an existing token with it, you can do the following:
 
 ```java
 OAuth2TokenResponse refreshToken = tokenFlows.refreshTokenFlow()
-                              .refreshToken("<<Your refresh token goes here.>>")
-                              .subdomain(jwtToken.getSubdomain()) // this is optional 
-                              .execute();
+                                        .refreshToken("<<Your refresh token goes here. You get this from the OAuth server.>>")
+                                        .execute();
 ```
-### User Token Flow
-In order to exchange a user token for another user access token:
+
+Finally, to create a **user token flow** (to exchange one Jwt token for another), you can do the following:
+
 ```java
 XsuaaToken jwtToken = SpringSecurityContext.getToken();
 
 OAuth2TokenResponse userToken = tokenFlows.userTokenFlow()
                 .token("<<Your current User access token goes here.>>")
-                .clientId("other's client id") // this is optional
+                .clientId("other's client id") // this is optional!
                 .subdomain(jwtToken.getSubdomain()) // this is optional      
-                .attributes(additionalAttributes) // this is optional
+                .attributes(additionalAttributes) // this is optional!
                 .execute();
 ```
 
 Make sure to read the API documentation of the `XsuaaTokenFlows` API, to understand what the individual token flows' parameters are for.
-Also note, that the **user token flow** requires an input token that has the scope `uaa.user` to succeed.
-
+Also note, that the **user token flow** requires an input token that has the scope `uaa.user` to succeed. 
 
-## Sample (Spring)
 Have a look at [`TestController.java`](/samples/spring-security-xsuaa-usage/src/main/java/sample/spring/xsuaa/TestController.java) for sample code.
diff --git a/token-client/src/main/java/com/sap/cloud/security/xsuaa/client/OAuth2TokenResponse.java b/token-client/src/main/java/com/sap/cloud/security/xsuaa/client/OAuth2TokenResponse.java
index ab626126..4305423f 100644
--- a/token-client/src/main/java/com/sap/cloud/security/xsuaa/client/OAuth2TokenResponse.java
+++ b/token-client/src/main/java/com/sap/cloud/security/xsuaa/client/OAuth2TokenResponse.java
@@ -1,5 +1,8 @@
 package com.sap.cloud.security.xsuaa.client;
 
+import com.sap.cloud.security.xsuaa.jwt.Base64JwtDecoder;
+import com.sap.cloud.security.xsuaa.jwt.DecodedJwt;
+
 import javax.annotation.Nullable;
 import java.util.Date;
 import java.util.concurrent.TimeUnit;
@@ -19,13 +22,26 @@ public class OAuth2TokenResponse {
 	 * An OAuth2 access token. This token will be a JSON Web Token suitable for
 	 * offline validation by OAuth2 Resource Servers.
 	 *
-	 * @return the access token
+	 * @return the encoded access token
 	 */
 	@Nullable
 	public String getAccessToken() {
 		return accessToken;
 	}
 
+	/**
+	 * A decoded OAuth2 access token.
+	 *
+	 * @return the decoded access token
+	 */
+	@Nullable
+	public DecodedJwt getDecodedAccessToken() {
+		if (accessToken == null) {
+			return null;
+		}
+		return new Base64JwtDecoder().decode(accessToken);
+	}
+
 	public Date getExpiredAtDate() {
 		return new Date(expiredTimeMillis);
 	}
diff --git a/token-client/src/main/java/com/sap/cloud/security/xsuaa/jwt/Base64JwtDecoder.java b/token-client/src/main/java/com/sap/cloud/security/xsuaa/jwt/Base64JwtDecoder.java
new file mode 100644
index 00000000..2ca0a21c
--- /dev/null
+++ b/token-client/src/main/java/com/sap/cloud/security/xsuaa/jwt/Base64JwtDecoder.java
@@ -0,0 +1,56 @@
+package com.sap.cloud.security.xsuaa.jwt;
+
+import com.sap.cloud.security.xsuaa.Assertions;
+
+import java.nio.charset.StandardCharsets;
+import java.util.Base64;
+
+public final class Base64JwtDecoder {
+
+	public DecodedJwt decode(String jwt) {
+		Assertions.assertNotNull(jwt, "JWT must not be null");
+
+		String[] parts = jwt.split("\\.");
+		if (parts.length != 3) {
+			throw new IllegalArgumentException("Failed to split JWT into exactly 3 parts");
+		}
+		String header = base64Decode(parts[0]);
+		String payload = base64Decode(parts[1]);
+		String signature = parts[2];
+
+		return new DecodedJwtImpl(header, payload, signature);
+	}
+
+	private String base64Decode(String encoded) {
+		byte[] decodedBytes = Base64.getDecoder().decode(encoded);
+		return new String(decodedBytes, StandardCharsets.UTF_8);
+	}
+
+	static class DecodedJwtImpl implements DecodedJwt {
+
+		private String header;
+		private String payload;
+		private String signature;
+
+		DecodedJwtImpl(String header, String payload, String signature) {
+			this.header = header;
+			this.payload = payload;
+			this.signature = signature;
+		}
+
+		@Override
+		public String getHeader() {
+			return header;
+		}
+
+		@Override
+		public String getPayload() {
+			return payload;
+		}
+
+		@Override
+		public String getSignature() {
+			return signature;
+		}
+	}
+}
diff --git a/token-client/src/main/java/com/sap/cloud/security/xsuaa/jwt/DecodedJwt.java b/token-client/src/main/java/com/sap/cloud/security/xsuaa/jwt/DecodedJwt.java
new file mode 100644
index 00000000..b05ebc66
--- /dev/null
+++ b/token-client/src/main/java/com/sap/cloud/security/xsuaa/jwt/DecodedJwt.java
@@ -0,0 +1,10 @@
+package com.sap.cloud.security.xsuaa.jwt;
+
+public interface DecodedJwt {
+
+	String getHeader();
+
+	String getPayload();
+
+	String getSignature();
+}


Test patch:
diff --git a/token-client/src/test/java/com/sap/cloud/security/xsuaa/jwt/Base64JwtDecoderTest.java b/token-client/src/test/java/com/sap/cloud/security/xsuaa/jwt/Base64JwtDecoderTest.java
new file mode 100644
index 00000000..59ffd5b4
--- /dev/null
+++ b/token-client/src/test/java/com/sap/cloud/security/xsuaa/jwt/Base64JwtDecoderTest.java
@@ -0,0 +1,40 @@
+package com.sap.cloud.security.xsuaa.jwt;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+
+import static org.junit.Assert.assertEquals;
+
+public class Base64JwtDecoderTest {
+
+	private String encodedJwt = "eyJhbGciOiJIUzI1NiIsImprdSI6Imh0dHBzOi8vYWNtZS1lbnRlcnByaXNlcy5hdXRoZW50aWNhdGlvbi5leGFtcGxlLmNvbS90b2tlbl9rZXlzIiwia2lkIjoia2V5LWlkLTEiLCJ0eXAiOiJKV1QifQ.eyJqdGkiOiJlM2MzMGUyNDc0Y2Q0NjYwOWEyNjJlZGE5ZDlkYzI2ZCIsImV4dF9hdHRyIjp7ImVuaGFuY2VyIjoiWFNVQUEiLCJ6ZG4iOiJhY21lLWVudGVycHJpc2VzIn0sInhzLnN5c3RlbS5hdHRyaWJ1dGVzIjp7InhzLnJvbGVjb2xsZWN0aW9ucyI6W119LCJnaXZlbl9uYW1lIjoiQW5kcmVhIE1hcmlhIiwieHMudXNlci5hdHRyaWJ1dGVzIjp7fSwiZmFtaWx5X25hbWUiOiJNaWxsc2FwIiwic3ViIjoiMTIzNCIsInNjb3BlIjpbIm9wZW5pZCIsInVhYS51c2VyIl0sImNsaWVudF9pZCI6Im15LWFwcDEiLCJjaWQiOiJteS1hcHAxIiwiYXpwIjoibXktYXBwMSIsImdyYW50X3R5cGUiOiJhdXRob3JpemF0aW9uX2NvZGUiLCJ1c2VyX2lkIjoiMTIzNCIsIm9yaWdpbiI6ImxkYXAiLCJ1c2VyX25hbWUiOiJhbS5taWxsc2FwQHNhcC5jb20iLCJlbWFpbCI6ImFtLm1pbGxzYXBAc2FwLmNvbSIsImF1dGhfdGltZSI6MTU2NDc4NTcyMCwicmV2X3NpZyI6Ijg3ZTdjMDE2IiwiaWF0IjoxNTY0Nzg1NzIwLCJleHAiOjE1NjQ3ODU3MjEsImlzcyI6Imh0dHA6Ly9hY21lLWVudGVycHJpc2VzLmV4YW1wbGUuY29tL3VhYS9vYXV0aC90b2tlbiIsInppZCI6IjIzNDUiLCJhdWQiOlsibXktYXBwMSIsInVhYSIsIm9wZW5pZCJdfQ.yqEcFR3EkzVSfVo3tfxsl9kc6KtCSe75-al5cTZbzhk";
+
+	@Rule
+	public ExpectedException expectedException = ExpectedException.none();
+
+	@Test
+	public void itDecodesAnEncodedJwtString() {
+		String expectedDecodedJWTPayload = "{\"jti\":\"e3c30e2474cd46609a262eda9d9dc26d\",\"ext_attr\":{\"enhancer\":\"XSUAA\",\"zdn\":\"acme-enterprises\"},\"xs.system.attributes\":{\"xs.rolecollections\":[]},\"given_name\":\"Andrea Maria\",\"xs.user.attributes\":{},\"family_name\":\"Millsap\",\"sub\":\"1234\",\"scope\":[\"openid\",\"uaa.user\"],\"client_id\":\"my-app1\",\"cid\":\"my-app1\",\"azp\":\"my-app1\",\"grant_type\":\"authorization_code\",\"user_id\":\"1234\",\"origin\":\"ldap\",\"user_name\":\"am.millsap@sap.com\",\"email\":\"am.millsap@sap.com\",\"auth_time\":1564785720,\"rev_sig\":\"87e7c016\",\"iat\":1564785720,\"exp\":1564785721,\"iss\":\"http://acme-enterprises.example.com/uaa/oauth/token\",\"zid\":\"2345\",\"aud\":[\"my-app1\",\"uaa\",\"openid\"]}";
+		String expectedDecodedJWTHeader = "{\"alg\":\"HS256\",\"jku\":\"https://acme-enterprises.authentication.example.com/token_keys\",\"kid\":\"key-id-1\",\"typ\":\"JWT\"}";
+		String expectedDecodedJWTSignature = "yqEcFR3EkzVSfVo3tfxsl9kc6KtCSe75-al5cTZbzhk";
+		DecodedJwt decodedJwt = new Base64JwtDecoder().decode(encodedJwt);
+		assertEquals(expectedDecodedJWTPayload, decodedJwt.getPayload());
+		assertEquals(expectedDecodedJWTHeader, decodedJwt.getHeader());
+		assertEquals(expectedDecodedJWTSignature, decodedJwt.getSignature());
+	}
+
+	@Test
+	public void itThrowsIfJwtDoesNotConsistOfThreeSegments() {
+		expectedException.expect(IllegalArgumentException.class);
+		expectedException.expectMessage("Failed to split JWT into exactly 3 parts");
+
+		new Base64JwtDecoder().decode("invalid");
+	}
+
+	@Test
+	public void itAllowsEmptyPayload() {
+		DecodedJwt decodedJwt = new Base64JwtDecoder().decode("header..signature");
+		assertEquals("", decodedJwt.getPayload());
+	}
+}