1313// limitations under the License.
1414'use strict' ;
1515
16- const async = require ( 'async' ) ;
1716const debug = require ( 'debug' ) ( 'cloudant:plugins:iamauth' ) ;
1817const request = require ( 'request' ) ;
1918const u = require ( 'url' ) ;
2019
2120const BasePlugin = require ( './base.js' ) ;
21+ const IAMTokenManager = require ( '../lib/tokens/IamTokenManager' ) ;
2222
2323/**
2424 * IAM Authentication plugin.
@@ -29,165 +29,77 @@ class IAMPlugin extends BasePlugin {
2929 throw new Error ( 'Missing IAM API key from configuration' ) ;
3030 }
3131
32- // token service retry configuration
3332 cfg = Object . assign ( {
34- retryDelayMultiplier : 2 ,
35- retryInitialDelayMsecs : 500
33+ autoRenew : true ,
34+ iamTokenUrl : 'https://iam.cloud.ibm.com/identity/token' ,
35+ retryDelayMsecs : 1000
3636 } , cfg ) ;
3737
3838 super ( client , cfg ) ;
3939
40- this . currentIamApiKey = null ;
41- this . baseUrl = cfg . baseUrl || null ;
42- this . cookieJar = null ;
43- this . tokenUrl = cfg . iamTokenUrl || 'https://iam.cloud.ibm.com/identity/token' ;
44- this . refreshRequired = true ;
40+ let sessionUrl = new u . URL ( cfg . serverUrl ) ;
41+ sessionUrl . pathname = '/_iam_session' ;
42+
43+ this . _jar = request . jar ( ) ;
44+
45+ this . _tokenManager = new IAMTokenManager (
46+ client ,
47+ this . _jar ,
48+ u . format ( sessionUrl , { auth : false } ) ,
49+ cfg . iamTokenUrl ,
50+ cfg . iamApiKey ,
51+ cfg . iamClientId ,
52+ cfg . iamClientSecret
53+ ) ;
54+
55+ if ( cfg . autoRenew ) {
56+ this . _tokenManager . startAutoRenew ( ) ;
57+ }
4558 }
4659
4760 onRequest ( state , req , callback ) {
4861 var self = this ;
4962
50- if ( self . _cfg . iamApiKey !== self . currentIamApiKey ) {
51- debug ( 'New IAM API key identified.' ) ;
52- self . currentIamApiKey = self . _cfg . iamApiKey ;
53- state . stash . newApiKey = true ;
54- } else if ( ! self . refreshRequired ) {
55- if ( self . baseUrl && self . cookieJar . getCookies ( self . baseUrl , { expire : true } ) . length === 0 ) {
56- debug ( 'There are no valid session cookies in the jar. Requesting IAM session refresh...' ) ;
57- } else {
58- req . jar = self . cookieJar ; // add jar
59- return callback ( state ) ;
60- }
61- }
62-
63- req . url = req . uri || req . url ;
64- delete req . uri ;
63+ req . jar = self . _jar ;
6564
66- if ( self . baseUrl === null ) {
67- var parsed = u . parse ( req . url ) ;
68- self . baseUrl = u . format ( {
69- protocol : parsed . protocol ,
70- host : parsed . host ,
71- port : parsed . port
72- } ) ;
73- }
65+ req . uri = req . uri || req . url ;
66+ delete req . url ;
67+ req . uri = u . format ( new u . URL ( req . uri ) , { auth : false } ) ;
7468
75- self . refreshCookie ( self . _cfg , state , function ( error ) {
76- if ( error ) {
77- debug ( error . message ) ;
78- if ( state . attempt < state . maxAttempt ) {
79- state . retry = true ;
80- if ( state . attempt === 1 ) {
81- state . retryDelayMsecs = self . _cfg . retryInitialDelayMsecs ;
82- } else {
83- state . retryDelayMsecs *= self . _cfg . retryDelayMultiplier ;
84- }
69+ self . _tokenManager . renewIfRequired ( ) . then ( ( ) => {
70+ callback ( state ) ;
71+ } ) . catch ( ( error ) => {
72+ debug ( error ) ;
73+ if ( state . attempt < state . maxAttempt ) {
74+ state . retry = true ;
75+ let iamResponse = error . response ;
76+ let retryAfterSecs ;
77+ if ( iamResponse && iamResponse . headers ) {
78+ retryAfterSecs = iamResponse . headers [ 'Retry-After' ] ;
79+ }
80+ if ( retryAfterSecs ) {
81+ state . retryDelayMsecs = retryAfterSecs * 1000 ;
8582 } else {
86- state . abortWithResponse = [ error ] ; // return error to client
83+ state . retryDelayMsecs = self . _cfg . retryDelayMsecs ;
8784 }
8885 } else {
89- req . jar = self . cookieJar ; // add jar
86+ state . abortWithResponse = [ error ] ; // return error to client
9087 }
9188 callback ( state ) ;
9289 } ) ;
9390 }
9491
9592 onResponse ( state , response , callback ) {
9693 if ( response . statusCode === 401 ) {
97- debug ( 'Requesting IAM session refresh for 401 response.' ) ;
98- this . refreshRequired = true ;
94+ debug ( 'Received 401 response. Asking for request retry.' ) ;
9995 state . retry = true ;
96+ this . _tokenManager . attemptTokenRenewal = true ;
10097 }
10198 callback ( state ) ;
10299 }
103100
104- // Perform IAM session request.
105- refreshCookie ( cfg , state , callback ) {
106- var self = this ;
107-
108- if ( self . baseUrl === null ) {
109- return callback ( new Error ( 'Unspecified base URL' ) ) ;
110- }
111-
112- self . withLock ( {
113- stale : cfg . iamLockStaleMsecs || 2500 , // 2.5 secs
114- wait : cfg . iamLockWaitMsecs || 2000 // 2 secs
115- } , function ( error , done ) {
116- if ( state . stash . newApiKey ) {
117- debug ( 'Refreshing session with new IAM API key.' ) ;
118- state . stash . newApiKey = false ;
119- self . cookieJar = request . jar ( ) ; // new jar
120- } else if ( ! self . refreshRequired ) {
121- debug ( 'Session refresh no longer required.' ) ;
122- return callback ( ) ;
123- }
124- debug ( 'Making IAM session request.' ) ;
125- var accessToken = null ;
126- async . series ( [
127- function ( callback ) {
128- var accessTokenAuth ;
129- if ( typeof cfg . iamClientId !== 'undefined' && typeof cfg . iamClientSecret !== 'undefined' ) {
130- accessTokenAuth = { user : cfg . iamClientId , pass : cfg . iamClientSecret } ;
131- }
132- // get access token
133- self . _client ( {
134- url : self . tokenUrl ,
135- method : 'POST' ,
136- auth : accessTokenAuth ,
137- headers : { 'Accepts' : 'application/json' } ,
138- form : {
139- 'grant_type' : 'urn:ibm:params:oauth:grant-type:apikey' ,
140- 'response_type' : 'cloud_iam' ,
141- 'apikey' : cfg . iamApiKey
142- } ,
143- json : true
144- } , function ( error , response , body ) {
145- if ( error ) {
146- callback ( error ) ;
147- } else if ( response . statusCode === 200 ) {
148- if ( body . access_token ) {
149- accessToken = body . access_token ;
150- debug ( 'Retrieved access token from IAM token service.' ) ;
151- callback ( ) ;
152- } else {
153- callback ( new Error ( 'Invalid response from IAM token service' ) ) ;
154- }
155- } else {
156- callback ( new Error ( `Failed to acquire access token. Status code: ${ response . statusCode } ` ) ) ;
157- }
158- } ) ;
159- } ,
160- function ( callback ) {
161- // perform IAM cookie based user login
162- self . _client ( {
163- url : self . baseUrl + '/_iam_session' ,
164- method : 'POST' ,
165- form : { 'access_token' : accessToken } ,
166- jar : self . cookieJar ,
167- json : true
168- } , function ( error , response , body ) {
169- if ( error ) {
170- callback ( error ) ;
171- } else if ( response . statusCode === 200 ) {
172- self . refreshRequired = false ;
173- debug ( 'Successfully renewed IAM session.' ) ;
174- callback ( ) ;
175- } else {
176- callback ( new Error ( `Failed to exchange IAM token with Cloudant. Status code: ${ response . statusCode } ` ) ) ;
177- }
178- } ) ;
179- }
180- ] ,
181- function ( error ) {
182- done ( ) ; // release lock
183- callback ( error ) ;
184- } ) ;
185- } ) ;
186- }
187-
188- setIamApiKey ( iamApiKey ) {
189- debug ( 'Setting new IAM API key.' ) ;
190- this . _cfg . iamApiKey = iamApiKey ;
101+ setIamApiKey ( newIamApiKey ) {
102+ this . _tokenManager . setIamApiKey ( newIamApiKey ) ;
191103 }
192104}
193105
0 commit comments