@@ -2,16 +2,16 @@ export class Authenticator {
22 constructor ( http , creds ) {
33 this . http = http ;
44 this . creds = creds ;
5- this . bearerToken = "" ;
5+ this . accessToken = "" ;
66 this . refreshToken = "" ;
7- this . expirationEpoch = 0
7+ this . expiresAt = 0
88 this . refreshRunning = false ;
99
1010 // If the authentication method is access token,
1111 // our bearer token is already available for use
1212 if ( this . creds instanceof AuthAccessTokenCredentials ) {
13- this . bearerToken = this . creds . accessToken ;
14- this . expirationEpoch = calcExpirationEpoch ( this . creds . expiresIn ) ;
13+ this . accessToken = this . creds . accessToken ;
14+ this . expiresAt = calcExpirationEpoch ( this . creds . expiresIn ) ;
1515 this . refreshToken = this . creds . refreshToken ;
1616 }
1717 }
@@ -27,14 +27,17 @@ export class Authenticator {
2727 case AuthAccessTokenCredentials :
2828 authenticator = new AccessTokenAuthenticator ( this . http , this . creds , config ) ;
2929 break ;
30+ case AuthClientCredentials :
31+ authenticator = new ClientCredentialsAuthenticator ( this . http , this . creds , config ) ;
32+ break ;
3033 default :
3134 throw new Error ( "unsupported credential type" ) ;
3235 }
3336
3437 return authenticator . refresh ( )
3538 . then ( resp => {
36- this . bearerToken = resp . bearerToken ;
37- this . expirationEpoch = resp . expirationEpoch ;
39+ this . accessToken = resp . accessToken ;
40+ this . expiresAt = resp . expiresAt ;
3841 this . refreshToken = resp . refreshToken ;
3942 if ( ! this . refreshRunning ) {
4043 this . runBackgroundTokenRefresh ( authenticator ) ;
@@ -47,20 +50,21 @@ export class Authenticator {
4750 return this . http . externalGet ( localConfig . href )
4851 . then ( openidProviderConfig => {
4952 return {
50- clientId : localConfig . clientId ,
51- provider : openidProviderConfig
53+ clientId : localConfig . clientId ,
54+ provider : openidProviderConfig ,
55+ scopes : localConfig . scopes
5256 } ;
5357 } ) ;
5458 } ;
5559
5660 runBackgroundTokenRefresh = ( authenticator ) => {
57- setInterval ( async ( ) => {
61+ setInterval ( async ( ) => {
5862 // check every 30s if the token will expire in <= 1m,
5963 // if so, refresh
60- if ( this . expirationEpoch - Date . now ( ) <= 60_000 ) {
64+ if ( this . expiresAt - Date . now ( ) <= 60_000 ) {
6165 var resp = await authenticator . refresh ( ) ;
62- this . bearerToken = resp . bearerToken ;
63- this . expirationEpoch = resp . expirationEpoch ;
66+ this . accessToken = resp . accessToken ;
67+ this . expiresAt = resp . expiresAt ;
6468 this . refreshToken = resp . refreshToken ;
6569 }
6670 } , 30_000 )
@@ -71,6 +75,7 @@ export class AuthUserPasswordCredentials {
7175 constructor ( creds ) {
7276 this . username = creds . username ;
7377 this . password = creds . password ;
78+ this . scopes = creds . scopes ;
7479 }
7580}
7681
@@ -79,15 +84,18 @@ class UserPasswordAuthenticator {
7984 this . http = http ;
8085 this . creds = creds ;
8186 this . openidConfig = config ;
87+ if ( creds . scopes ) {
88+ this . openidConfig . scopes . push ( creds . scopes ) ;
89+ }
8290 }
8391
8492 refresh = ( ) => {
8593 this . validateOpenidConfig ( ) ;
8694 return this . requestAccessToken ( )
8795 . then ( tokenResp => {
8896 return {
89- bearerToken : tokenResp . access_token ,
90- expirationEpoch : calcExpirationEpoch ( tokenResp . expires_in ) ,
97+ accessToken : tokenResp . access_token ,
98+ expiresAt : calcExpirationEpoch ( tokenResp . expires_in ) ,
9199 refreshToken : tokenResp . refresh_token
92100 } ;
93101 } )
@@ -98,37 +106,38 @@ class UserPasswordAuthenticator {
98106 } ) ;
99107 } ;
100108
109+ validateOpenidConfig = ( ) => {
110+ if ( this . openidConfig . provider . grant_types_supported !== undefined &&
111+ ! this . openidConfig . provider . grant_types_supported . includes ( "password" ) ) {
112+ throw new Error ( "grant_type password not supported" ) ;
113+ }
114+ if ( this . openidConfig . provider . token_endpoint . includes (
115+ "https://login.microsoftonline.com" ) ) {
116+ throw new Error ( "microsoft/azure recommends to avoid authentication using " +
117+ "username and password, so this method is not supported by this client" ) ;
118+ }
119+ this . openidConfig . scopes . push ( "offline_access" ) ;
120+ } ;
121+
101122 requestAccessToken = ( ) => {
102123 var url = this . openidConfig . provider . token_endpoint ;
103124 var params = new URLSearchParams ( {
104125 grant_type : "password" ,
105126 client_id : this . openidConfig . clientId ,
106127 username : this . creds . username ,
107128 password : this . creds . password ,
108- scope : "openid offline_access"
129+ scope : this . openidConfig . scopes . join ( " " )
109130 } ) ;
110131 let contentType = "application/x-www-form-urlencoded;charset=UTF-8" ;
111132 return this . http . externalPost ( url , params , contentType ) ;
112133 } ;
113-
114- validateOpenidConfig = ( ) => {
115- if ( this . openidConfig . provider . grant_types_supported !== undefined &&
116- ! this . openidConfig . provider . grant_types_supported . includes ( "password" ) ) {
117- throw new Error ( "grant_type password not supported" ) ;
118- }
119- if ( this . openidConfig . provider . token_endpoint . includes (
120- "https://login.microsoftonline.com" ) ) {
121- throw new Error ( "microsoft/azure recommends to avoid authentication using " +
122- "username and password, so this method is not supported by this client" ) ;
123- }
124- } ;
125134}
126135
127136export class AuthAccessTokenCredentials {
128137 constructor ( creds ) {
129138 this . validate ( creds ) ;
130139 this . accessToken = creds . accessToken ;
131- this . expirationEpoch = calcExpirationEpoch ( creds . expiresIn ) ;
140+ this . expiresAt = calcExpirationEpoch ( creds . expiresIn ) ;
132141 this . refreshToken = creds . refreshToken ;
133142 }
134143
@@ -153,16 +162,16 @@ class AccessTokenAuthenticator {
153162 if ( this . creds . refreshToken === undefined || this . creds . refreshToken == "" ) {
154163 console . warn ( "AuthAccessTokenCredentials not provided with refreshToken, cannot refresh" ) ;
155164 return Promise . resolve ( {
156- bearerToken : this . creds . accessToken ,
157- expirationEpoch : this . creds . expirationEpoch
165+ accessToken : this . creds . accessToken ,
166+ expiresAt : this . creds . expiresAt
158167 } ) ;
159168 }
160169 this . validateOpenidConfig ( ) ;
161170 return this . requestAccessToken ( )
162171 . then ( tokenResp => {
163172 return {
164- bearerToken : tokenResp . access_token ,
165- expirationEpoch : calcExpirationEpoch ( tokenResp . expires_in ) ,
173+ accessToken : tokenResp . access_token ,
174+ expiresAt : calcExpirationEpoch ( tokenResp . expires_in ) ,
166175 refreshToken : tokenResp . refresh_token
167176 } ;
168177 } )
@@ -176,8 +185,8 @@ class AccessTokenAuthenticator {
176185 validateOpenidConfig = ( ) => {
177186 if ( this . openidConfig . provider . grant_types_supported === undefined ||
178187 ! this . openidConfig . provider . grant_types_supported . includes ( "refresh_token" ) ) {
179- throw new Error ( "grant_type refresh_token not supported" ) ;
180- }
188+ throw new Error ( "grant_type refresh_token not supported" ) ;
189+ }
181190 } ;
182191
183192 requestAccessToken = ( ) => {
@@ -192,6 +201,66 @@ class AccessTokenAuthenticator {
192201 } ;
193202}
194203
204+ export class AuthClientCredentials {
205+ constructor ( creds ) {
206+ this . clientSecret = creds . clientSecret ;
207+ this . scopes = creds . scopes ;
208+ }
209+ }
210+
211+ class ClientCredentialsAuthenticator {
212+ constructor ( http , creds , config ) {
213+ this . http = http ;
214+ this . creds = creds ;
215+ this . openidConfig = config ;
216+ if ( creds . scopes ) {
217+ this . openidConfig . scopes . push ( creds . scopes ) ;
218+ }
219+ }
220+
221+ refresh = ( ) => {
222+ this . validateOpenidConfig ( ) ;
223+ return this . requestAccessToken ( )
224+ . then ( tokenResp => {
225+ return {
226+ accessToken : tokenResp . access_token ,
227+ expiresAt : calcExpirationEpoch ( tokenResp . expires_in ) ,
228+ refreshToken : tokenResp . refresh_token
229+ } ;
230+ } )
231+ . catch ( err => {
232+ return Promise . reject (
233+ new Error ( `failed to refresh access token: ${ err } ` )
234+ ) ;
235+ } ) ;
236+ } ;
237+
238+ validateOpenidConfig = ( ) => {
239+ this . openidConfig . scopes = this . openidConfig . scopes
240+ . filter ( scope => scope != "openid" && scope != "email" ) ;
241+ if ( this . openidConfig . scopes . length > 0 ) {
242+ return ;
243+ }
244+ if ( this . openidConfig . provider . token_endpoint
245+ . includes ( "https://login.microsoftonline.com" ) ) {
246+ this . openidConfig . scopes . push ( this . openidConfig . clientId + "/.default" ) ;
247+ }
248+ } ;
249+
250+ requestAccessToken = ( ) => {
251+ var url = this . openidConfig . provider . token_endpoint ;
252+ var params = new URLSearchParams ( {
253+ grant_type : "client_credentials" ,
254+ client_id : this . openidConfig . clientId ,
255+ client_secret : this . creds . clientSecret ,
256+ scope : this . openidConfig . scopes . join ( " " )
257+ } ) ;
258+
259+ let contentType = "application/x-www-form-urlencoded;charset=UTF-8" ;
260+ return this . http . externalPost ( url , params , contentType ) ;
261+ } ;
262+ }
263+
195264function calcExpirationEpoch ( expiresIn ) {
196265 return Date . now ( ) + ( ( expiresIn - 2 ) * 1000 ) // -2 for some lag
197266}
0 commit comments