diff --git a/common4j/src/main/com/microsoft/identity/common/java/jwt/AbstractJwtRequest.java b/common4j/src/main/com/microsoft/identity/common/java/jwt/AbstractJwtRequest.java index 5baed3a477..eb93d3bdcb 100644 --- a/common4j/src/main/com/microsoft/identity/common/java/jwt/AbstractJwtRequest.java +++ b/common4j/src/main/com/microsoft/identity/common/java/jwt/AbstractJwtRequest.java @@ -62,6 +62,8 @@ public static class ClaimNames { public static final String JWE_CRYPTO = "jwe_crypto"; public static final String SESSION_KEY_CRYPTO = "session_key_crypto"; public static final String PURPOSE = "purpose"; + + public static final String DEVICE_TOKEN = "device_token"; } @SerializedName(ClaimNames.REFRESH_TOKEN) diff --git a/common4j/src/main/com/microsoft/identity/common/java/jwt/JwtRequestBody.java b/common4j/src/main/com/microsoft/identity/common/java/jwt/JwtRequestBody.java index 58a2a43bc2..36dc4773b2 100644 --- a/common4j/src/main/com/microsoft/identity/common/java/jwt/JwtRequestBody.java +++ b/common4j/src/main/com/microsoft/identity/common/java/jwt/JwtRequestBody.java @@ -88,6 +88,9 @@ public final class JwtRequestBody extends AbstractJwtRequest { @SerializedName(ClaimNames.PURPOSE) private String mPurpose; + @SerializedName(ClaimNames.DEVICE_TOKEN) + private String mDeviceToken; + public void setIat(final long iat) { mIat = String.valueOf(iat); } diff --git a/common4j/src/main/com/microsoft/identity/common/java/jwt/JwtRequestHeader.java b/common4j/src/main/com/microsoft/identity/common/java/jwt/JwtRequestHeader.java index 35cea03973..bb19f26ff1 100644 --- a/common4j/src/main/com/microsoft/identity/common/java/jwt/JwtRequestHeader.java +++ b/common4j/src/main/com/microsoft/identity/common/java/jwt/JwtRequestHeader.java @@ -45,6 +45,8 @@ public final class JwtRequestHeader extends AbstractJwtRequest { // RSA using SHA256 - asymmetric key signing algorithm public static final String ALG_VALUE_RS256 = "RS256"; + public static final String KID_VALUE_ECDH = "ecdh"; + @Setter(AccessLevel.NONE) @SerializedName(ClaimNames.TYPE) private String mType; diff --git a/common4j/src/main/com/microsoft/identity/common/java/jwt/JwtUtils.java b/common4j/src/main/com/microsoft/identity/common/java/jwt/JwtUtils.java index 377cf207bb..10c8c7a583 100644 --- a/common4j/src/main/com/microsoft/identity/common/java/jwt/JwtUtils.java +++ b/common4j/src/main/com/microsoft/identity/common/java/jwt/JwtUtils.java @@ -28,6 +28,13 @@ import com.google.gson.Gson; import com.microsoft.identity.common.java.base64.Base64Util; import com.microsoft.identity.common.java.logging.Logger; +import com.microsoft.identity.common.java.providers.oauth2.TokenRequest; +import com.microsoft.identity.common.java.util.MsaUtil; + +import java.util.Arrays; + + +import javax.annotation.Nullable; import lombok.NonNull; import lombok.experimental.UtilityClass; @@ -59,5 +66,38 @@ public static String generateJWT(@NonNull final JwtRequestHeader header, Base64Util.encodeUrlSafeString(headerJson.getBytes(ENCODING_UTF8)) + "." + Base64Util.encodeUrlSafeString(bodyJson.getBytes(ENCODING_UTF8)); return encodedJwt; } + + /** + * Generate a JWT request header to be used in MSA DR flows. + * @param keyContext key context to include in header + * @return a request headers object + */ + @NonNull + public static JwtRequestHeader generateJwtRequestHeaderForMsaDR(@Nullable final byte[] keyContext) { + final JwtRequestHeader jwtRequestHeader = new JwtRequestHeader(); + jwtRequestHeader.setType(); + jwtRequestHeader.setAlg(JwtRequestHeader.ALG_VALUE_HS256); + jwtRequestHeader.setKId(JwtRequestHeader.KID_VALUE_ECDH); + jwtRequestHeader.setCtx(Arrays.toString(keyContext)); + + return jwtRequestHeader; + } + + /** + * Generate a JWT request body to be used in MSA DR flows. + * @param deviceToken device token to include in body + * @return a request body object + */ + @NonNull + public static JwtRequestBody generateJwtRequestBodyForMsaDR(@Nullable final String audienceUrl, @Nullable final String nonce, @Nullable final String deviceToken) { + final JwtRequestBody jwtRequestBody = new JwtRequestBody(); + jwtRequestBody.setAudience(audienceUrl); + jwtRequestBody.setNonce(nonce); + jwtRequestBody.setPurpose(MsaUtil.Companion.getJwtPurpose()); + jwtRequestBody.setGrantType(TokenRequest.GrantTypes.DEVICE_AUTH); + jwtRequestBody.setDeviceToken(deviceToken); + + return jwtRequestBody; + } } diff --git a/common4j/src/main/com/microsoft/identity/common/java/providers/oauth2/TokenRequest.java b/common4j/src/main/com/microsoft/identity/common/java/providers/oauth2/TokenRequest.java index 6d15116cc8..5174ba9af9 100644 --- a/common4j/src/main/com/microsoft/identity/common/java/providers/oauth2/TokenRequest.java +++ b/common4j/src/main/com/microsoft/identity/common/java/providers/oauth2/TokenRequest.java @@ -305,6 +305,7 @@ public static class GrantTypes { // e.g. used in Primary Refresh Token acquisition flows public static final String JWT_BEARER = "urn:ietf:params:oauth:grant-type:jwt-bearer"; public static final String TRANSFER_TOKEN = "transfer_token"; + public static final String DEVICE_AUTH = "device_auth"; } public static class TokenType { diff --git a/common4j/src/main/com/microsoft/identity/common/java/util/MsaUtil.kt b/common4j/src/main/com/microsoft/identity/common/java/util/MsaUtil.kt new file mode 100644 index 0000000000..ffc63cf312 --- /dev/null +++ b/common4j/src/main/com/microsoft/identity/common/java/util/MsaUtil.kt @@ -0,0 +1,53 @@ +// Copyright (c) Microsoft Corporation. +// All rights reserved. +// +// This code is licensed under the MIT License. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files(the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and / or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions : +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +package com.microsoft.identity.common.java.util + +import com.microsoft.identity.common.java.authorities.Authority +import com.microsoft.identity.common.java.authorities.AzureActiveDirectoryAudience +import com.microsoft.identity.common.java.authorities.AzureActiveDirectoryAuthority +import java.util.Locale + +/** + * Class for various MSA-related utility methods. + */ +class MsaUtil { + + companion object { + val jwtPurpose = "v2sso" + + /** + * Given an authority, check if this is an MSA request + */ + fun isMsaRequest(authority : Authority) : Boolean { + return if (authority !is AzureActiveDirectoryAuthority) { + false + } else { + // authority has been silently casted to AzureActiveDirectoryAuthority + val audience = authority.mAudience.tenantId.lowercase(Locale.ROOT) + + // Check if audience is consumers or the MSA Mega Tenant + audience == AzureActiveDirectoryAudience.MSA_MEGA_TENANT_ID || audience == AzureActiveDirectoryAudience.CONSUMERS + } + } + } +} diff --git a/common4j/src/test/com/microsoft/identity/common/java/jwt/JwtUtilsTest.java b/common4j/src/test/com/microsoft/identity/common/java/jwt/JwtUtilsTest.java index 6d4fb039f5..45f230b732 100644 --- a/common4j/src/test/com/microsoft/identity/common/java/jwt/JwtUtilsTest.java +++ b/common4j/src/test/com/microsoft/identity/common/java/jwt/JwtUtilsTest.java @@ -28,12 +28,15 @@ import com.google.gson.Gson; import com.microsoft.identity.common.java.base64.Base64Util; import com.microsoft.identity.common.java.providers.oauth2.TokenRequest; +import com.microsoft.identity.common.java.util.MsaUtil; import org.junit.Assert; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; +import java.util.Arrays; + @RunWith(JUnit4.class) public class JwtUtilsTest { @@ -59,4 +62,34 @@ public void testGenerateJWT() { final String encodedJwt = JwtUtils.generateJWT(jwtRequestHeader, jwtRequestBody); Assert.assertEquals(expectedJwt, encodedJwt);; } + + @Test + public void testGenerateJwtForMsaDR() { + final byte[] mockContext = "mockContext".getBytes(); + final String mockAudience = "mockAudience"; + final String mockNonce = "mockNonce"; + final String mockDeviceToken = "mockDeviceToken"; + + final JwtRequestHeader expectedJwtRequestHeader = new JwtRequestHeader(); + expectedJwtRequestHeader.setAlg(JwtRequestHeader.ALG_VALUE_HS256); + expectedJwtRequestHeader.setKId(JwtRequestHeader.KID_VALUE_ECDH); + expectedJwtRequestHeader.setCtx(Arrays.toString(mockContext)); + final JwtRequestBody expectedJwtRequestBody = new JwtRequestBody(); + expectedJwtRequestBody.setAudience(mockAudience); + expectedJwtRequestBody.setNonce(mockNonce); + expectedJwtRequestBody.setPurpose(MsaUtil.Companion.getJwtPurpose()); + expectedJwtRequestBody.setGrantType(TokenRequest.GrantTypes.DEVICE_AUTH); + expectedJwtRequestBody.setDeviceToken(mockDeviceToken); + final String headerJson = new Gson().toJson(expectedJwtRequestHeader); + final String bodyJson = new Gson().toJson(expectedJwtRequestBody); + + final String expectedJwt = + Base64Util.encodeUrlSafeString(headerJson.getBytes(ENCODING_UTF8)) + "." + Base64Util.encodeUrlSafeString(bodyJson.getBytes(ENCODING_UTF8)); + + final String encodedJwt = JwtUtils.generateJWT( + JwtUtils.generateJwtRequestHeaderForMsaDR(mockContext), + JwtUtils.generateJwtRequestBodyForMsaDR(mockAudience, mockNonce, mockDeviceToken) + ); + Assert.assertEquals(expectedJwt, encodedJwt); + } } diff --git a/common4j/src/test/com/microsoft/identity/common/java/util/MsaUtilTest.kt b/common4j/src/test/com/microsoft/identity/common/java/util/MsaUtilTest.kt new file mode 100644 index 0000000000..994ef5c539 --- /dev/null +++ b/common4j/src/test/com/microsoft/identity/common/java/util/MsaUtilTest.kt @@ -0,0 +1,61 @@ +// Copyright (c) Microsoft Corporation. +// All rights reserved. +// +// This code is licensed under the MIT License. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files(the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and / or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions : +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +package com.microsoft.identity.common.java.util + +import com.microsoft.identity.common.java.authorities.AccountsInOneOrganization +import com.microsoft.identity.common.java.authorities.AzureActiveDirectoryAudience +import com.microsoft.identity.common.java.authorities.AzureActiveDirectoryAuthority +import com.microsoft.identity.common.java.authorities.AzureActiveDirectoryB2CAuthority +import org.junit.Assert +import org.junit.Test + +class MsaUtilTest { + + @Test + fun isMsaRequestWithMsaMegaTenantAuthority() { + val msaMegaTenantAuthority = AzureActiveDirectoryAuthority(AccountsInOneOrganization(AzureActiveDirectoryAudience.MSA_MEGA_TENANT_ID)) + + Assert.assertTrue(MsaUtil.isMsaRequest(msaMegaTenantAuthority)) + } + + @Test + fun isMsaRequestWithConsumersAuthority() { + val consumersTenantAuthority = AzureActiveDirectoryAuthority(AccountsInOneOrganization(AzureActiveDirectoryAudience.CONSUMERS)) + + Assert.assertTrue(MsaUtil.isMsaRequest(consumersTenantAuthority)) + } + + @Test + fun isMsaRequestWithNonMsaAuthority() { + val nonMsaAuthority = AzureActiveDirectoryAuthority(AccountsInOneOrganization(AzureActiveDirectoryAudience.ALL)) + + Assert.assertFalse(MsaUtil.isMsaRequest(nonMsaAuthority)) + } + + @Test + fun isMsaRequestWithNonAzureAuthority() { + val nonAzureActiveDirectoryAuthority = AzureActiveDirectoryB2CAuthority("mockUrl") + + Assert.assertFalse(MsaUtil.isMsaRequest(nonAzureActiveDirectoryAuthority)) + } +}