44package com .microsoft .aad .msal4j ;
55
66import java .nio .charset .StandardCharsets ;
7+ import java .security .InvalidKeyException ;
78import java .security .Signature ;
9+ import java .security .spec .MGF1ParameterSpec ;
10+ import java .security .spec .PSSParameterSpec ;
811import java .util .ArrayList ;
912import java .util .Base64 ;
1013import java .util .HashMap ;
@@ -22,62 +25,183 @@ static ClientAssertion buildJwt(String clientId, final ClientCertificate credent
2225 ParameterValidationUtils .validateNotNull ("credential" , clientId );
2326
2427 try {
25- final long time = System .currentTimeMillis ();
26-
27- // Build header
28- Map <String , Object > header = new HashMap <>();
29- header .put ("alg" , "RS256" );
30- header .put ("typ" , "JWT" );
31-
32- if (sendX5c ) {
33- List <String > certs = new ArrayList <>(credential .getEncodedPublicKeyCertificateChain ());
34- header .put ("x5c" , certs );
35- }
36-
37- //SHA-256 is preferred, however certain flows still require SHA-1 due to what is supported server-side. If SHA-256
38- // is not supported or the IClientCredential.publicCertificateHash256() method is not implemented, the library will default to SHA-1.
39- String hash256 = credential .publicCertificateHash256 ();
40- if (useSha1 || hash256 == null ) {
41- header .put ("x5t" , credential .publicCertificateHash ());
42- } else {
43- header .put ("x5t#S256" , hash256 );
28+ // First try with PS256 (preferred)
29+ return generatePS256Jwt (clientId , credential , jwtAudience , sendX5c , useSha1 );
30+ } catch (InvalidKeyException e ) {
31+ // If the key isn't compatible with PSS, fall back to RS256.
32+ // This is for backwards compatibility, as the Signature instance created with SHA256withRSA
33+ // accepted key types that weren't RSAPrivateKey but the RSASSA-PSS signature does not.
34+ try {
35+ return generateRs256Jwt (clientId , credential , jwtAudience , sendX5c , useSha1 );
36+ } catch (Exception fallbackException ) {
37+ throw new MsalClientException (fallbackException );
4438 }
39+ } catch (Exception e ) {
40+ throw new MsalClientException (e );
41+ }
42+ }
4543
46- // Build payload
47- Map <String , Object > payload = new HashMap <>();
48- payload .put ("aud" , jwtAudience );
49- payload .put ("iss" , clientId );
50- payload .put ("jti" , UUID .randomUUID ().toString ());
51- payload .put ("nbf" , time / 1000 );
52- payload .put ("exp" , time / 1000 + Constants .AAD_JWT_TOKEN_LIFETIME_SECONDS );
53- payload .put ("sub" , clientId );
44+ /**
45+ * Generates a JWT signed using the PS256 algorithm (RSASSA-PSS with SHA-256).
46+ *
47+ * @param clientId The client ID to use as the issuer and subject
48+ * @param credential The certificate credential used for signing
49+ * @param jwtAudience The audience claim for the JWT
50+ * @param sendX5c Whether to include the x5c header with certificate chain
51+ * @param useSha1 Whether to use SHA-1 hash for thumbprint instead of SHA-256
52+ * @return A ClientAssertion containing the signed JWT
53+ * @throws Exception If JWT creation or signing fails
54+ */
55+ private static ClientAssertion generatePS256Jwt (String clientId , ClientCertificate credential ,
56+ String jwtAudience , boolean sendX5c ,
57+ boolean useSha1 ) throws Exception {
58+ // Build header with PS256 algorithm
59+ Map <String , Object > header = createHeader (credential , sendX5c , useSha1 , "PS256" );
60+
61+ // Build payload
62+ Map <String , Object > payload = createPayload (clientId , jwtAudience , System .currentTimeMillis ());
63+
64+ // Encode header and payload
65+ String jsonHeader = JsonHelper .writeJsonMap (header );
66+ String jsonPayload = JsonHelper .writeJsonMap (payload );
67+ String encodedHeader = base64UrlEncode (jsonHeader .getBytes (StandardCharsets .UTF_8 ));
68+ String encodedPayload = base64UrlEncode (jsonPayload .getBytes (StandardCharsets .UTF_8 ));
69+ String dataToSign = encodedHeader + "." + encodedPayload ;
70+
71+ // Sign with PS256
72+ byte [] signatureBytes = signWithPS256 (credential , dataToSign );
73+ String encodedSignature = base64UrlEncode (signatureBytes );
74+
75+ // Build the JWT
76+ String jwt = dataToSign + "." + encodedSignature ;
77+ return new ClientAssertion (jwt );
78+ }
5479
55- // Concatenate header and payload
56- String jsonHeader = JsonHelper .writeJsonMap (header );
57- String jsonPayload = JsonHelper .writeJsonMap (payload );
80+ /**
81+ * Generates a JWT signed using the RS256 algorithm (RSASSA-PKCS1-v1_5 with SHA-256).
82+ * This is used as a fallback when PS256 is not supported by the private key.
83+ *
84+ * @param clientId The client ID to use as the issuer and subject
85+ * @param credential The certificate credential used for signing
86+ * @param jwtAudience The audience claim for the JWT
87+ * @param sendX5c Whether to include the x5c header with certificate chain
88+ * @param useSha1 Whether to use SHA-1 hash for thumbprint instead of SHA-256
89+ * @return A ClientAssertion containing the signed JWT
90+ * @throws Exception If JWT creation or signing fails
91+ */
92+ private static ClientAssertion generateRs256Jwt (String clientId , ClientCertificate credential ,
93+ String jwtAudience , boolean sendX5c ,
94+ boolean useSha1 ) throws Exception {
95+ // Build header with RS256 algorithm
96+ Map <String , Object > header = createHeader (credential , sendX5c , useSha1 , "RS256" );
97+
98+ // Build payload
99+ Map <String , Object > payload = createPayload (clientId , jwtAudience , System .currentTimeMillis ());
100+
101+ // Encode header and payload
102+ String jsonHeader = JsonHelper .writeJsonMap (header );
103+ String jsonPayload = JsonHelper .writeJsonMap (payload );
104+ String encodedHeader = base64UrlEncode (jsonHeader .getBytes (StandardCharsets .UTF_8 ));
105+ String encodedPayload = base64UrlEncode (jsonPayload .getBytes (StandardCharsets .UTF_8 ));
106+ String dataToSign = encodedHeader + "." + encodedPayload ;
107+
108+ // Sign with RS256
109+ byte [] signatureBytes = signWithRS256 (credential , dataToSign );
110+ String encodedSignature = base64UrlEncode (signatureBytes );
111+
112+ // Build the JWT
113+ String jwt = dataToSign + "." + encodedSignature ;
114+ return new ClientAssertion (jwt );
115+ }
58116
59- String encodedHeader = base64UrlEncode (jsonHeader .getBytes (StandardCharsets .UTF_8 ));
60- String encodedPayload = base64UrlEncode (jsonPayload .getBytes (StandardCharsets .UTF_8 ));
117+ /**
118+ * Creates the JWT header with the specified algorithm and certificate information.
119+ *
120+ * @param credential The certificate credential containing thumbprint and chain
121+ * @param sendX5c Whether to include the x5c header with certificate chain
122+ * @param useSha1 Whether to use SHA-1 hash for thumbprint instead of SHA-256
123+ * @param algorithm The signing algorithm to specify in the header (PS256 or RS256)
124+ * @return A map containing the JWT header claims
125+ * @throws Exception If certificate operations fail
126+ */
127+ private static Map <String , Object > createHeader (ClientCertificate credential , boolean sendX5c ,
128+ boolean useSha1 , String algorithm ) throws Exception {
129+ Map <String , Object > header = new HashMap <>();
130+ header .put ("alg" , algorithm );
131+ header .put ("typ" , "JWT" );
132+
133+ if (sendX5c ) {
134+ List <String > certs = new ArrayList <>(credential .getEncodedPublicKeyCertificateChain ());
135+ header .put ("x5c" , certs );
136+ }
61137
62- // Create signature
63- String dataToSign = encodedHeader + "." + encodedPayload ;
138+ // SHA-256 is preferred, however certain flows still require SHA-1
139+ String hash256 = credential .publicCertificateHash256 ();
140+ if (useSha1 || hash256 == null ) {
141+ header .put ("x5t" , credential .publicCertificateHash ());
142+ } else {
143+ header .put ("x5t#S256" , hash256 );
144+ }
64145
65- Signature sig = Signature .getInstance ("SHA256withRSA" );
66- sig .initSign (credential .privateKey ());
67- sig .update (dataToSign .getBytes (StandardCharsets .UTF_8 ));
68- byte [] signatureBytes = sig .sign ();
146+ return header ;
147+ }
69148
70- String encodedSignature = base64UrlEncode (signatureBytes );
149+ /**
150+ * Creates the JWT payload with standard claims.
151+ *
152+ * @param clientId The client ID to use as the issuer and subject
153+ * @param audience The audience claim for the JWT
154+ * @param time The current time in milliseconds
155+ * @return A map containing the JWT payload claims
156+ */
157+ private static Map <String , Object > createPayload (String clientId , String audience , long time ) {
158+ Map <String , Object > payload = new HashMap <>();
159+ payload .put ("aud" , audience );
160+ payload .put ("iss" , clientId );
161+ payload .put ("jti" , UUID .randomUUID ().toString ());
162+ payload .put ("nbf" , time / 1000 );
163+ payload .put ("exp" , time / 1000 + Constants .AAD_JWT_TOKEN_LIFETIME_SECONDS );
164+ payload .put ("sub" , clientId );
165+ return payload ;
166+ }
71167
72- // Build the JWT
73- String jwt = dataToSign + "." + encodedSignature ;
168+ /**
169+ * Signs data using the PS256 algorithm (RSASSA-PSS with SHA-256).
170+ *
171+ * @param credential The certificate credential containing the private key
172+ * @param dataToSign The data to sign
173+ * @return The signature bytes
174+ * @throws Exception If signing fails
175+ */
176+ private static byte [] signWithPS256 (ClientCertificate credential , String dataToSign ) throws Exception {
177+ Signature sig = Signature .getInstance ("RSASSA-PSS" );
178+ sig .setParameter (new PSSParameterSpec ("SHA-256" , "MGF1" , MGF1ParameterSpec .SHA256 , 32 , 1 ));
179+ sig .initSign (credential .privateKey ());
180+ sig .update (dataToSign .getBytes (StandardCharsets .UTF_8 ));
181+ return sig .sign ();
182+ }
74183
75- return new ClientAssertion (jwt );
76- } catch (final Exception e ) {
77- throw new MsalClientException (e );
78- }
184+ /**
185+ * Signs data using the RS256 algorithm (RSASSA-PKCS1-v1_5 with SHA-256).
186+ *
187+ * @param credential The certificate credential containing the private key
188+ * @param dataToSign The data to sign
189+ * @return The signature bytes
190+ * @throws Exception If signing fails
191+ */
192+ private static byte [] signWithRS256 (ClientCertificate credential , String dataToSign ) throws Exception {
193+ Signature sig = Signature .getInstance ("SHA256withRSA" );
194+ sig .initSign (credential .privateKey ());
195+ sig .update (dataToSign .getBytes (StandardCharsets .UTF_8 ));
196+ return sig .sign ();
79197 }
80198
199+ /**
200+ * Encodes bytes using Base64URL encoding without padding.
201+ *
202+ * @param data The data to encode
203+ * @return The Base64URL encoded string
204+ */
81205 private static String base64UrlEncode (byte [] data ) {
82206 return Base64 .getUrlEncoder ().withoutPadding ().encodeToString (data );
83207 }
0 commit comments