2626package org .eclipse .digitaltwin .basyx .client .internal .authorization ;
2727
2828import java .io .IOException ;
29+ import java .text .ParseException ;
2930
3031import org .eclipse .digitaltwin .basyx .client .internal .authorization .grant .AccessTokenProvider ;
3132import org .eclipse .digitaltwin .basyx .core .exceptions .AccessTokenRetrievalException ;
33+ import org .slf4j .Logger ;
34+ import org .slf4j .LoggerFactory ;
3235
3336import com .nimbusds .oauth2 .sdk .AccessTokenResponse ;
3437import com .nimbusds .oauth2 .sdk .token .AccessToken ;
3538import com .nimbusds .oauth2 .sdk .token .RefreshToken ;
3639
40+ import net .minidev .json .JSONObject ;
41+ import com .nimbusds .jwt .JWT ;
42+ import com .nimbusds .jwt .JWTParser ;
43+ import com .nimbusds .jwt .SignedJWT ;
44+
45+ import java .util .Date ;
46+
3747/**
3848 * Requests and manages the Access Tokens and Refresh Tokens.
3949 *
40- * @author danish
50+ * @author danish
4151 */
52+ import java .time .Instant ;
53+
4254public class TokenManager {
43-
44- private String tokenEndpoint ;
45- private AccessTokenProvider accessTokenProvider ;
55+
56+ private static final Logger LOGGER = LoggerFactory .getLogger (TokenManager .class );
57+
58+ private static final String EXPIRES_IN = "expires_in" ;
59+ private static final String REFRESH_EXPIRES_IN = "refresh_expires_in" ;
60+ private final String tokenEndpoint ;
61+ private final AccessTokenProvider accessTokenProvider ;
4662 private String accessToken ;
47- private String refreshToken ;
48- private long accessTokenExpiryTime ;
49- private long refreshTokenExpiryTime ;
50-
63+ private String refreshToken ;
64+ private Instant accessTokenExpiryTime ;
65+ private Instant refreshTokenExpiryTime ;
66+
5167 public TokenManager (String tokenEndpoint , AccessTokenProvider accessTokenProvider ) {
52- super ();
5368 this .tokenEndpoint = tokenEndpoint ;
5469 this .accessTokenProvider = accessTokenProvider ;
5570 }
@@ -61,46 +76,139 @@ public String getTokenEndpoint() {
6176 public AccessTokenProvider getAccessTokenProvider () {
6277 return this .accessTokenProvider ;
6378 }
64-
79+
80+ /**
81+ * Provides the access token, refreshing it if necessary.
82+ *
83+ * @return the current valid access token
84+ * @throws IOException
85+ * if an error occurs while retrieving the token
86+ */
87+ public String getAccessToken () throws IOException {
88+ Instant currentTime = Instant .now ();
89+
90+ if (accessToken != null && currentTime .isBefore (accessTokenExpiryTime ))
91+ return accessToken ;
92+
93+ synchronized (this ) {
94+ if (accessToken != null && currentTime .isBefore (accessTokenExpiryTime ))
95+ return accessToken ;
96+
97+ if (refreshToken != null && currentTime .isBefore (refreshTokenExpiryTime ))
98+ return refreshAccessToken (currentTime );
99+
100+ return obtainNewAccessToken (currentTime );
101+ }
102+ }
103+
65104 /**
66- * Provides access token
105+ * Updates the tokens and their expiry times.
67106 *
68- * @return accessToken
107+ * @param accessTokenResponse
108+ * the response containing the new tokens
109+ * @param currentTime
110+ * the current timestamp for consistency
111+ * @return the new access token
69112 * @throws IOException
113+ * if an error occurs while processing the response
114+ */
115+ private String updateTokens (AccessTokenResponse accessTokenResponse , Instant currentTime ) throws IOException {
116+ AccessToken accessTokenObj = accessTokenResponse .getTokens ().getAccessToken ();
117+ accessToken = accessTokenObj .getValue ();
118+ accessTokenExpiryTime = calculateExpiryTime (accessTokenObj , currentTime );
119+
120+ RefreshToken refreshTokenObj = accessTokenResponse .getTokens ().getRefreshToken ();
121+
122+ if (refreshTokenObj != null ) {
123+ refreshToken = refreshTokenObj .getValue ();
124+ refreshTokenExpiryTime = calculateRefreshExpiryTime (refreshTokenObj , accessTokenResponse , currentTime );
125+ }
126+
127+ return accessToken ;
128+ }
129+
130+ /**
131+ * Calculates the expiry time for a JWT token. First checks the 'exp' field in
132+ * the JWT, falling back to 'expires_in'.
133+ *
134+ * @param tokenObj
135+ * the AccessToken or RefreshToken object
136+ * @param currentTime
137+ * the current timestamp
138+ * @return the calculated expiry time as Instant
139+ */
140+ private Instant calculateExpiryTime (AccessToken tokenObj , Instant currentTime ) {
141+ String tokenValue = tokenObj .getValue ();
142+ Date expirationDate = extractExpirationTimeAsDateFromToken (tokenValue );
143+
144+ if (expirationDate != null )
145+ return expirationDate .toInstant ();
146+
147+ LOGGER .info ("Unable to find 'exp' claim inside Access Token! Falling back to the alternative, the '{}' field." , EXPIRES_IN );
148+
149+ return currentTime .plusSeconds (tokenObj .getLifetime ());
150+ }
151+
152+ /**
153+ * Calculates the expiry time for a refresh token. First checks the 'exp' field
154+ * in the JWT refresh token, falling back to 'refresh_expires_in'.
155+ *
156+ * @param refreshTokenObj
157+ * the RefreshToken object
158+ * @param accessTokenResponse
159+ * the response containing the refresh token
160+ * @param currentTime
161+ * the current timestamp
162+ * @return the calculated expiry time as Instant
70163 */
71- public synchronized String getAccessToken () throws IOException {
164+ private Instant calculateRefreshExpiryTime (RefreshToken refreshTokenObj , AccessTokenResponse accessTokenResponse , Instant currentTime ) {
165+ String tokenValue = refreshTokenObj .getValue ();
166+ Date expirationDate = extractExpirationTimeAsDateFromToken (tokenValue );
167+
168+ if (expirationDate != null )
169+ return expirationDate .toInstant ();
170+
171+ LOGGER .info ("Unable to find 'exp' claim inside Refresh Token! Falling back to the alternative, the '{}' field" , REFRESH_EXPIRES_IN );
72172
73- if ( accessToken != null && System . currentTimeMillis () < accessTokenExpiryTime )
74- return accessToken ;
173+ JSONObject jsonObject = accessTokenResponse . toJSONObject ();
174+ Number refreshExpiresInSeconds = jsonObject . getAsNumber ( REFRESH_EXPIRES_IN ) ;
75175
76- if (refreshToken != null && System .currentTimeMillis () < refreshTokenExpiryTime ) {
77- try {
78- return requestAccessToken (accessTokenProvider .getAccessTokenResponse (tokenEndpoint , refreshToken ));
79- } catch (IOException e ) {
80- throw new AccessTokenRetrievalException ("Error occurred while retrieving access token" + e .getMessage ());
176+ if (refreshExpiresInSeconds == null )
177+ return Instant .EPOCH ;
178+
179+ return currentTime .plusSeconds (refreshExpiresInSeconds .longValue ());
180+ }
181+
182+ private Date extractExpirationTimeAsDateFromToken (String tokenValue ) {
183+ try {
184+ JWT jwt = JWTParser .parse (tokenValue );
185+
186+ if (jwt instanceof SignedJWT ) {
187+ SignedJWT signedJwt = (SignedJWT ) jwt ;
188+ return signedJwt .getJWTClaimsSet ().getExpirationTime ();
81189 }
82- }
190+ } catch (ParseException e ) {
191+ LOGGER .error ("Failed to parse the token. Invalid JWT format: " + e .getMessage ());
192+ } catch (Exception e ) {
193+ LOGGER .error ("Unexpected error occurred while extracting expiration time from the Token: " + e .getMessage ());
194+ }
195+
196+ return null ;
197+ }
198+
199+ private String obtainNewAccessToken (Instant currentTime ) {
200+ try {
201+ return updateTokens (accessTokenProvider .getAccessTokenResponse (tokenEndpoint ), currentTime );
202+ } catch (IOException e ) {
203+ throw new AccessTokenRetrievalException ("Error occurred while retrieving access token: " + e .getMessage ());
204+ }
205+ }
83206
84- try {
85- return requestAccessToken (accessTokenProvider .getAccessTokenResponse (tokenEndpoint ));
207+ private String refreshAccessToken (Instant currentTime ) {
208+ try {
209+ return updateTokens (accessTokenProvider .getAccessTokenResponse (tokenEndpoint , refreshToken ), currentTime );
86210 } catch (IOException e ) {
87- throw new AccessTokenRetrievalException ("Error occurred while retrieving access token" + e .getMessage ());
211+ throw new AccessTokenRetrievalException ("Error occurred while retrieving access token: " + e .getMessage ());
88212 }
89- }
90-
91- private String requestAccessToken (AccessTokenResponse accessTokenResponse ) throws IOException {
92- AccessToken accessTokenObj = accessTokenResponse .getTokens ().getAccessToken ();
93- accessToken = accessTokenObj .getValue ();
94- accessTokenExpiryTime = accessTokenObj .getLifetime ();
95-
96- RefreshToken refreshTokenObj = accessTokenResponse .getTokens ().getRefreshToken ();
97-
98- if (refreshTokenObj != null ) {
99- refreshToken = refreshTokenObj .getValue ();
100- refreshTokenExpiryTime = System .currentTimeMillis () + (30L * 24L * 60L * 60L * 1000L );
101- }
102-
103- return accessToken ;
104- }
105-
213+ }
106214}
0 commit comments