Skip to content
This repository was archived by the owner on Jul 5, 2023. It is now read-only.

Commit 7eb3b0e

Browse files
authored
Merge pull request #221 from AzureAD/dev
Release 1.6.0
2 parents 28c3922 + 53c2338 commit 7eb3b0e

9 files changed

Lines changed: 169 additions & 54 deletions

File tree

README.md

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,19 +3,29 @@
33
# Microsoft Azure Active Directory Authentication Library (ADAL) for Java
44
=====================================
55

6-
## Samples and Documentation
6+
|[Getting Started](https://github.com/AzureAD/azure-activedirectory-library-for-java/wiki)| [Docs](https://aka.ms/aaddev)| [Samples](https://github.com/azure-samples?query=active-directory)| [Support](README.md#community-help-and-support)
7+
| --- | --- | --- | --- |
78

8-
[We provide a full suite of sample applications and documentation on GitHub](https://github.com/Azure-Samples) to help you get started with learning the Azure Identity system. This includes tutorials for native clients such as Windows, Windows Phone, iOS, macOS, Android, and Linux. We also provide full walkthroughs for authentication flows such as OAuth2, OpenID Connect, Graph API, and other awesome features.
9+
The ADAL for Java library enables Java applications to authenticate with Azure AD and get tokens to access Azure AD protected web resources.
10+
11+
You can learn in detail about ADAL4J functionality and usage documented in the [Wiki](https://github.com/AzureAD/azure-activedirectory-library-for-java/wiki).
912

1013
## Versions
11-
Current version - 1.5.0
14+
Current version - 1.6.0
1215

13-
Minimum recommended version - 1.5.0
16+
Minimum recommended version - 1.6.0
1417

1518
From version 1.3.0 support for handling Conditional Access claims challenge was added. You can read about CA [here](https://go.microsoft.com/fwlink/?linkid=855860) and refer this [sample](https://github.com/AzureAD/azure-activedirectory-library-for-java/tree/dev/src/samples/web-app-samples-for-adal4j) to handle it.
1619

1720
You can find the changes for each version in the [change log](https://github.com/AzureAD/azure-activedirectory-library-for-java/blob/master/changelog.txt).
1821

22+
## Contribution
23+
All code is licensed under the MIT License and we triage actively on GitHub. We encourage and welcome contributions to the library. Please read the [contributing guide](./contributing.md) before starting.
24+
25+
## Samples and Documentation
26+
27+
[We provide a full suite of sample applications and documentation on GitHub](https://github.com/Azure-Samples) to help you get started with learning the Azure Identity system. This includes tutorials for native clients such as Windows, Windows Phone, iOS, macOS, Android, and Linux. We also provide full walkthroughs for authentication flows such as OAuth2, OpenID Connect, Graph API, and other awesome features.
28+
1929
## Logging
2030

2131
ADAL for Java uses the Simple Logging Facade for Java (SLF4J) as a simple facade or abstraction for various logging frameworks.
@@ -42,10 +52,6 @@ We recommend you use the "adal" tag so we can see it! Here is the latest Q&A on
4252

4353
If you find a security issue with our libraries or services please report it to [secure@microsoft.com](mailto:secure@microsoft.com) with as much detail as possible. Your submission may be eligible for a bounty through the [Microsoft Bounty](http://aka.ms/bugbounty) program. Please do not post security issues to GitHub Issues or any other public site. We will contact you shortly upon receiving the information. We encourage you to get notifications of when security incidents occur by visiting [this page](https://technet.microsoft.com/en-us/security/dd252948) and subscribing to Security Advisory Alerts.
4454

45-
## Contributing
46-
47-
All code is licensed under the MIT License and we triage actively on GitHub. We enthusiastically welcome contributions and feedback. You can clone the repo and start contributing now.
48-
4955
## We Value and Adhere to the Microsoft Open Source Code of Conduct
5056

5157
This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments.

changelog.txt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
1+
Version 1.6.0
2+
=============
3+
- added support for certificates in on-behalf-of api
4+
- added jti (JWT ID) claim to JWT
5+
16
Version 1.5.0
27
=============
38
- added device code flow api

pom.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
<modelVersion>4.0.0</modelVersion>
44
<groupId>com.microsoft.azure</groupId>
55
<artifactId>adal4j</artifactId>
6-
<version>1.5.0</version>
6+
<version>1.6.0</version>
77
<packaging>jar</packaging>
88
<name>adal4j</name>
99
<description>

src/main/java/com/microsoft/aad/adal4j/AuthenticationContext.java

Lines changed: 80 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -242,16 +242,6 @@ public Future<AuthenticationResult> acquireToken(final String resource,
242242
return this.acquireToken(authGrant, clientAuth, callback);
243243
}
244244

245-
private void validateInput(final String resource, final Object credential,
246-
final boolean validateResource) {
247-
if (validateResource && StringHelper.isBlank(resource)) {
248-
throw new IllegalArgumentException("resource is null or empty");
249-
}
250-
if (credential == null) {
251-
throw new IllegalArgumentException("credential is null");
252-
}
253-
}
254-
255245
/**
256246
* Acquires an access token from the authority on behalf of a user. It
257247
* requires using a user token previously received.
@@ -275,25 +265,65 @@ public Future<AuthenticationResult> acquireToken(final String resource,
275265
final UserAssertion userAssertion, final ClientCredential credential,
276266
final AuthenticationCallback callback) {
277267

278-
this.validateInput(resource, credential, true);
268+
this.validateOnBehalfOfRequestInput(resource, userAssertion, credential, true);
269+
final ClientAuthentication clientAuth = new ClientSecretPost(
270+
new ClientID(credential.getClientId()), new Secret(
271+
credential.getClientSecret()));
272+
return acquireTokenOnBehalfOf(resource, userAssertion, clientAuth, callback);
273+
}
274+
275+
/**
276+
* Acquires an access token from the authority on behalf of a user. It
277+
* requires using a user token previously received. Uses certificate to
278+
* authenticate client.
279+
*
280+
* @param resource
281+
* Identifier of the target resource that is the recipient of the
282+
* requested token.
283+
* @param userAssertion
284+
* userAssertion to use as Authorization grant
285+
* @param credential
286+
* The certificate based client credential to use for token acquisition.
287+
* @param callback
288+
* optional callback object for non-blocking execution.
289+
* @return A {@link Future} object representing the
290+
* {@link AuthenticationResult} of the call. It contains Access
291+
* Token and the Access Token's expiration time. Refresh Token
292+
* property will be null for this overload.
293+
* @throws AuthenticationException {@link AuthenticationException}
294+
*/
295+
public Future<AuthenticationResult> acquireToken(final String resource,
296+
final UserAssertion userAssertion,
297+
final AsymmetricKeyCredential credential,
298+
final AuthenticationCallback callback) {
299+
300+
this.validateOnBehalfOfRequestInput(resource, userAssertion, credential, true);
301+
ClientAssertion clientAssertion = JwtHelper
302+
.buildJwt(credential, this.authenticationAuthority.getSelfSignedJwtAudience());
303+
final ClientAuthentication clientAuth = createClientAuthFromClientAssertion(clientAssertion);
304+
return acquireTokenOnBehalfOf(resource, userAssertion, clientAuth, callback);
305+
}
306+
307+
private Future<AuthenticationResult> acquireTokenOnBehalfOf(final String resource,
308+
final UserAssertion userAssertion,
309+
final ClientAuthentication clientAuthentication,
310+
final AuthenticationCallback callback) {
311+
279312
Map<String, String> params = new HashMap<String, String>();
280313
params.put("resource", resource);
281314
params.put("requested_token_use", "on_behalf_of");
282315
try {
283316
AdalOAuthAuthorizationGrant grant = new AdalOAuthAuthorizationGrant(
284-
new JWTBearerGrant(
285-
SignedJWT.parse(userAssertion.getAssertion())), params);
317+
new JWTBearerGrant(SignedJWT.parse(userAssertion.getAssertion())), params);
286318

287-
final ClientAuthentication clientAuth = new ClientSecretPost(
288-
new ClientID(credential.getClientId()), new Secret(
289-
credential.getClientSecret()));
290-
return this.acquireToken(grant, clientAuth, callback);
319+
return this.acquireToken(grant, clientAuthentication, callback);
291320
}
292321
catch (final Exception e) {
293322
throw new AuthenticationException(e);
294323
}
295324
}
296325

326+
297327
/**
298328
* Acquires security token from the authority.
299329
*
@@ -829,21 +859,6 @@ private Future<AuthenticationResult> acquireToken(
829859
new AcquireTokenCallable(this, authGrant, clientAuth, callback));
830860
}
831861

832-
private void validateDeviceCodeRequestInput(String clientId, String resource) {
833-
if (StringHelper.isBlank(clientId)) {
834-
throw new IllegalArgumentException("clientId is null or empty");
835-
}
836-
837-
if (StringHelper.isBlank(resource)) {
838-
throw new IllegalArgumentException("resource is null or empty");
839-
}
840-
841-
if (AuthorityType.ADFS.equals(authenticationAuthority.getAuthorityType())){
842-
throw new IllegalArgumentException(
843-
"Invalid authority type. Device Flow is not supported by ADFS authority");
844-
}
845-
}
846-
847862
/**
848863
* Acquires a security token from the authority using a Refresh Token
849864
* previously received. This method is suitable for the daemon OAuth2
@@ -969,21 +984,44 @@ public String getAuthority() {
969984
return this.authority;
970985
}
971986

987+
private void validateInput(final String resource, final Object credential,
988+
final boolean validateResource) {
989+
if (validateResource && StringHelper.isBlank(resource)) {
990+
throw new IllegalArgumentException("resource is null or empty");
991+
}
992+
if (credential == null) {
993+
throw new IllegalArgumentException("credential is null");
994+
}
995+
}
996+
972997
private void validateAuthCodeRequestInput(final String authorizationCode,
973998
final URI redirectUri, final Object clientCredential,
974999
final String resource) {
9751000
if (StringHelper.isBlank(authorizationCode)) {
9761001
throw new IllegalArgumentException(
9771002
"authorization code is null or empty");
9781003
}
979-
9801004
if (redirectUri == null) {
9811005
throw new IllegalArgumentException("redirect uri is null");
9821006
}
983-
9841007
this.validateInput(resource, clientCredential, false);
9851008
}
9861009

1010+
private void validateDeviceCodeRequestInput(String clientId, String resource) {
1011+
if (StringHelper.isBlank(clientId)) {
1012+
throw new IllegalArgumentException("clientId is null or empty");
1013+
}
1014+
1015+
if (StringHelper.isBlank(resource)) {
1016+
throw new IllegalArgumentException("resource is null or empty");
1017+
}
1018+
1019+
if (AuthorityType.ADFS.equals(authenticationAuthority.getAuthorityType())){
1020+
throw new IllegalArgumentException(
1021+
"Invalid authority type. Device Flow is not supported by ADFS authority");
1022+
}
1023+
}
1024+
9871025
private void validateDeviceCodeRequestInput(final DeviceCode deviceCode,
9881026
final Object credential,
9891027
final String resource) {
@@ -995,4 +1033,12 @@ private void validateDeviceCodeRequestInput(final DeviceCode deviceCode,
9951033
}
9961034
this.validateInput(resource, credential, true);
9971035
}
1036+
1037+
private void validateOnBehalfOfRequestInput(final String resource, final UserAssertion userAssertion,
1038+
final Object clientCredential, final boolean validateResource) {
1039+
if (userAssertion == null) {
1040+
throw new IllegalArgumentException("userAssertion is null");
1041+
}
1042+
this.validateInput(resource, clientCredential, validateResource);
1043+
}
9981044
}

src/main/java/com/microsoft/aad/adal4j/JwtHelper.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
import java.util.Collections;
2828
import java.util.Date;
2929
import java.util.List;
30+
import java.util.UUID;
3031

3132
import com.nimbusds.jose.JWSAlgorithm;
3233
import com.nimbusds.jose.JWSHeader;
@@ -59,6 +60,7 @@ static ClientAssertion buildJwt(final AsymmetricKeyCredential credential,
5960
final JWTClaimsSet claimsSet = new JWTClaimsSet.Builder()
6061
.audience(Collections.singletonList(jwtAudience))
6162
.issuer(credential.getClientId())
63+
.jwtID(UUID.randomUUID().toString())
6264
.notBeforeTime(new Date(time))
6365
.expirationTime(new Date(time
6466
+ AuthenticationConstants.AAD_JWT_TOKEN_LIFETIME_SECONDS

src/samples/public-client-app-sample/pom.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
<dependency>
1616
<groupId>com.microsoft.azure</groupId>
1717
<artifactId>adal4j</artifactId>
18-
<version>1.5.0</version>
18+
<version>1.6.0</version>
1919
</dependency>
2020
<dependency>
2121
<groupId>com.nimbusds</groupId>

src/samples/public-client-device-code-sample/pom.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
<dependency>
2020
<groupId>com.microsoft.azure</groupId>
2121
<artifactId>adal4j</artifactId>
22-
<version>1.5.0</version>
22+
<version>1.6.0</version>
2323
</dependency>
2424
<dependency>
2525
<groupId>com.nimbusds</groupId>

src/samples/web-app-samples-for-adal4j/pom.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
<dependency>
1616
<groupId>com.microsoft.azure</groupId>
1717
<artifactId>adal4j</artifactId>
18-
<version>1.5.0</version>
18+
<version>1.6.0</version>
1919
</dependency>
2020
<dependency>
2121
<groupId>com.nimbusds</groupId>

src/test/java/com/microsoft/aad/adal4j/OAuthRequestValidationTest.java

Lines changed: 64 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -23,32 +23,43 @@
2323

2424
package com.microsoft.aad.adal4j;
2525

26+
import static org.powermock.api.support.membermodification.MemberMatcher.method;
27+
import static org.powermock.api.support.membermodification.MemberModifier.replace;
28+
29+
import java.io.FileInputStream;
2630
import java.io.UnsupportedEncodingException;
31+
import java.lang.reflect.InvocationHandler;
2732
import java.lang.reflect.Method;
2833
import java.net.MalformedURLException;
2934
import java.net.URLDecoder;
3035
import java.security.KeyPair;
3136
import java.security.KeyPairGenerator;
37+
import java.security.KeyStore;
3238
import java.security.NoSuchAlgorithmException;
39+
import java.security.PrivateKey;
40+
import java.security.cert.X509Certificate;
3341
import java.security.interfaces.RSAPrivateKey;
3442
import java.security.interfaces.RSAPublicKey;
35-
import java.util.*;
43+
import java.util.ArrayList;
44+
import java.util.Date;
45+
import java.util.LinkedHashMap;
46+
import java.util.List;
47+
import java.util.Map;
48+
import java.util.UUID;
3649
import java.util.concurrent.ExecutionException;
3750
import java.util.concurrent.ExecutorService;
3851
import java.util.concurrent.Executors;
3952
import java.util.concurrent.Future;
4053

41-
import com.nimbusds.jose.*;
54+
import com.nimbusds.jose.JOSEException;
55+
import com.nimbusds.jose.JWSAlgorithm;
56+
import com.nimbusds.jose.JWSHeader;
57+
import com.nimbusds.jose.JWSSigner;
4258
import com.nimbusds.jose.crypto.RSASSASigner;
4359
import com.nimbusds.jwt.JWTClaimsSet;
4460
import com.nimbusds.jwt.SignedJWT;
45-
import static org.powermock.api.support.membermodification.MemberMatcher.method;
46-
import static org.powermock.api.support.membermodification.MemberModifier.replace;
47-
61+
import org.apache.commons.lang3.StringUtils;
4862
import org.powermock.core.classloader.annotations.PrepareForTest;
49-
50-
import java.lang.reflect.InvocationHandler;
51-
5263
import org.powermock.modules.testng.PowerMockTestCase;
5364
import org.testng.Assert;
5465
import org.testng.annotations.AfterClass;
@@ -67,6 +78,7 @@ public class OAuthRequestValidationTest extends PowerMockTestCase {
6778

6879
private final static String GRANT_TYPE_JWT = "urn:ietf:params:oauth:grant-type:jwt-bearer";
6980
private final static String CLIENT_ASSERTION_TYPE_JWT = "urn:ietf:params:oauth:client-assertion-type:jwt-bearer";
81+
private final static String ON_BEHALF_OF_USE_JWT = "on_behalf_of";
7082

7183
private final static String CLIENT_CREDENTIALS_GRANT_TYPE = "client_credentials";
7284

@@ -178,6 +190,50 @@ public void oAuthRequest_for_acquireTokenByUserAssertion() throws Exception {
178190
Assert.assertEquals(RESOURCE, queryParams.get("resource"));
179191
}
180192

193+
@Test
194+
public void oAuthRequest_for_acquireTokenByAsymmetricKeyCredential() throws Exception {
195+
try {
196+
final KeyStore keystore = KeyStore.getInstance("PKCS12", "SunJSSE");
197+
keystore.load(
198+
new FileInputStream(this.getClass()
199+
.getResource(TestConfiguration.AAD_CERTIFICATE_PATH)
200+
.getFile()),
201+
TestConfiguration.AAD_CERTIFICATE_PASSWORD.toCharArray());
202+
final String alias = keystore.aliases().nextElement();
203+
final PrivateKey key = (PrivateKey) keystore.getKey(alias,
204+
TestConfiguration.AAD_CERTIFICATE_PASSWORD.toCharArray());
205+
final X509Certificate cert = (X509Certificate) keystore
206+
.getCertificate(alias);
207+
208+
AsymmetricKeyCredential certCredential = AsymmetricKeyCredential.create(CLIENT_ID, key, cert);
209+
210+
// Using UserAssertion as Authorization Grants
211+
Future<AuthenticationResult> future = context.acquireToken(RESOURCE, new UserAssertion(jwt),
212+
certCredential, null);
213+
future.get();
214+
}
215+
catch (ExecutionException ex){
216+
Assert.assertTrue(ex.getCause() instanceof AuthenticationException);
217+
}
218+
219+
Map<String, String> queryParams = splitQuery(query);
220+
Assert.assertEquals(7, queryParams.size());
221+
222+
// validate Authorization Grants query params
223+
Assert.assertEquals(GRANT_TYPE_JWT, queryParams.get("grant_type"));
224+
Assert.assertEquals(jwt, queryParams.get("assertion"));
225+
226+
// validate Client Authentication query params
227+
Assert.assertFalse(StringUtils.isEmpty(queryParams.get("client_assertion")));
228+
229+
Assert.assertEquals(OPEN_ID_SCOPE, queryParams.get("scope"));
230+
231+
Assert.assertEquals(CLIENT_ASSERTION_TYPE_JWT, queryParams.get("client_assertion_type"));
232+
Assert.assertEquals(ON_BEHALF_OF_USE_JWT, queryParams.get("requested_token_use"));
233+
234+
Assert.assertEquals(RESOURCE, queryParams.get("resource"));
235+
}
236+
181237
@Test
182238
public void oAuthRequest_for_acquireTokenByClientAssertion() throws Exception {
183239
String rsaJwt = getRSAjwt();

0 commit comments

Comments
 (0)