-
Notifications
You must be signed in to change notification settings - Fork 388
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: implements the OAuth token exchange spec based on rfc8693 #1026
Changes from 1 commit
c3415f1
11b0cb7
0052db2
4df48df
2dd4ae5
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,179 @@ | ||
// Copyright 2020 Google LLC | ||
// | ||
// Licensed under the Apache License, Version 2.0 (the "License"); | ||
// you may not use this file except in compliance with the License. | ||
// You may obtain a copy of the License at | ||
// | ||
// http://www.apache.org/licenses/LICENSE-2.0 | ||
// | ||
// Unless required by applicable law or agreed to in writing, software | ||
// distributed under the License is distributed on an "AS IS" BASIS, | ||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
// See the License for the specific language governing permissions and | ||
// limitations under the License. | ||
|
||
import {GaxiosOptions, GaxiosResponse} from 'gaxios'; | ||
import * as querystring from 'querystring'; | ||
|
||
import {DefaultTransporter} from '../transporters'; | ||
import {Headers} from './oauth2client'; | ||
import { | ||
ClientAuthentication, | ||
OAuthClientAuthHandler, | ||
OAuthErrorResponse, | ||
getErrorFromOAuthErrorResponse, | ||
} from './oauth2common'; | ||
|
||
/** | ||
* Defines the interface needed to initialize an StsCredentials instance. | ||
* The interface does not directly map to the spec and instead is converted | ||
* to be compliant with the JavaScript style guide. This is because this is | ||
* instantiated internally. | ||
* StsCredentials implement the OAuth 2.0 token exchange based on | ||
* https://tools.ietf.org/html/rfc8693. | ||
* Request options are defined in | ||
* https://tools.ietf.org/html/rfc8693#section-2.1 | ||
*/ | ||
export interface StsCredentialsOptions { | ||
grantType: string; | ||
resource?: string; | ||
audience?: string; | ||
scope?: string[]; | ||
requestedTokenType?: string; | ||
subjectToken: string; | ||
subjectTokenType: string; | ||
actingParty?: { | ||
actorToken: string; | ||
actorTokenType: string; | ||
}; | ||
} | ||
|
||
/** | ||
* Defines the standard request options as defined by the OAuth token | ||
* exchange spec: https://tools.ietf.org/html/rfc8693#section-2.1 | ||
*/ | ||
interface StsRequestOptions { | ||
grant_type: string; | ||
resource?: string; | ||
audience?: string; | ||
scope?: string; | ||
requested_token_type?: string; | ||
subject_token: string; | ||
subject_token_type: string; | ||
actor_token?: string; | ||
actor_token_type?: string; | ||
client_id?: string; | ||
client_secret?: string; | ||
[key: string]: string | undefined; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That is less than ideal :/ It effectively means any string key is value here with a string value. Is this really an open property bag? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ok. Currently there is only one non-standard field here. I manually added it. |
||
} | ||
|
||
/** | ||
* Defines the OAuth 2.0 token exchange successful response based on | ||
* https://tools.ietf.org/html/rfc8693#section-2.2.1 | ||
*/ | ||
export interface StsSuccessfulResponse { | ||
access_token: string; | ||
issued_token_type: string; | ||
token_type: string; | ||
expires_in: number; | ||
refresh_token?: string; | ||
scope: string; | ||
res?: GaxiosResponse | null; | ||
} | ||
|
||
/** | ||
* Implements the OAuth 2.0 token exchange based on | ||
* https://tools.ietf.org/html/rfc8693 | ||
*/ | ||
export class StsCredentials extends OAuthClientAuthHandler { | ||
private transporter: DefaultTransporter; | ||
|
||
/** | ||
* Initializes an STS credentials instance. | ||
* @param tokenExchangeEndpoint The token exchange endpoint. | ||
* @param clientAuthentication The client authentication credentials if | ||
* available. | ||
*/ | ||
constructor( | ||
private readonly tokenExchangeEndpoint: string, | ||
clientAuthentication?: ClientAuthentication | ||
) { | ||
super(clientAuthentication); | ||
this.transporter = new DefaultTransporter(); | ||
} | ||
|
||
/** | ||
* Exchanges the provided token for another type of token based on the | ||
* rfc8693 spec. | ||
* @param stsCredentialsOptions The token exchange options used to populate | ||
* the token exchange request. | ||
* @param additionalHeaders Optional additional headers to pass along the | ||
* request. | ||
* @param options Optional additional GCP-specific non-spec defined options | ||
* to send with the request. | ||
* Example: `&options=${encodeUriComponent(JSON.stringified(options))}` | ||
* @return A promise that resolves with the token exchange response containing | ||
* the requested token and its expiration time. | ||
*/ | ||
async exchangeToken( | ||
stsCredentialsOptions: StsCredentialsOptions, | ||
additionalHeaders?: Headers, | ||
// eslint-disable-next-line @typescript-eslint/no-explicit-any | ||
options?: {[key: string]: any} | ||
): Promise<StsSuccessfulResponse> { | ||
const values: StsRequestOptions = { | ||
grant_type: stsCredentialsOptions.grantType, | ||
resource: stsCredentialsOptions.resource, | ||
audience: stsCredentialsOptions.audience, | ||
scope: stsCredentialsOptions.scope?.join(' '), | ||
requested_token_type: stsCredentialsOptions.requestedTokenType, | ||
subject_token: stsCredentialsOptions.subjectToken, | ||
subject_token_type: stsCredentialsOptions.subjectTokenType, | ||
actor_token: stsCredentialsOptions.actingParty?.actorToken, | ||
actor_token_type: stsCredentialsOptions.actingParty?.actorTokenType, | ||
// Non-standard GCP-specific options. | ||
options: options && JSON.stringify(options), | ||
}; | ||
// Remove undefined fields. | ||
Object.keys(values).forEach(key => { | ||
if (typeof values[key] === 'undefined') { | ||
delete values[key]; | ||
} | ||
}); | ||
|
||
const headers = { | ||
'Content-Type': 'application/x-www-form-urlencoded', | ||
}; | ||
// Inject additional STS headers if available. | ||
Object.assign(headers, additionalHeaders || {}); | ||
|
||
const opts: GaxiosOptions = { | ||
url: this.tokenExchangeEndpoint, | ||
method: 'POST', | ||
headers, | ||
data: querystring.stringify(values), | ||
responseType: 'json', | ||
}; | ||
// Apply OAuth client authentication. | ||
this.applyClientAuthenticationOptions(opts); | ||
|
||
try { | ||
const response = await this.transporter.request<StsSuccessfulResponse>( | ||
opts | ||
); | ||
// Successful response. | ||
const stsSuccessfulResponse = response.data; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm curious - why return the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Mostly following the existing pattern established here and elsewhere in that file. |
||
stsSuccessfulResponse.res = response; | ||
return stsSuccessfulResponse; | ||
} catch (error) { | ||
// Translate error to OAuthError. | ||
if (error.response) { | ||
throw getErrorFromOAuthErrorResponse( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Just to double check - does this method create a There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It was creating a new error and not preserving the original error data. I have extended this to preserve the original error data (including stack) but to modify the error message. |
||
error.response.data as OAuthErrorResponse | ||
); | ||
} | ||
// Request could fail before the server responds. | ||
throw error; | ||
} | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't know if the spec lays these out clearly, but I like to pull whatever text they have (if of appropriate value and length) to include in comments above the interface properties. Like:
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done