From fa09290f1132a264060b8329c8d85f5566fba028 Mon Sep 17 00:00:00 2001 From: Jason McNair Date: Fri, 12 Jan 2018 15:48:46 -0600 Subject: [PATCH 1/2] adding Dropbox OAuth --- auth-helper-dropbox.ts | 33 +++++++++++++++++++ demo-angular/app/main.ts | 8 +++++ index.d.ts | 1 + index.ts | 18 +++++++++++ tns-oauth-interfaces.d.ts | 7 ++++ tns-oauth.ts | 67 ++++++++++++++++++++++++++------------- tsconfig.json | 1 + 7 files changed, 113 insertions(+), 22 deletions(-) create mode 100644 auth-helper-dropbox.ts diff --git a/auth-helper-dropbox.ts b/auth-helper-dropbox.ts new file mode 100644 index 0000000..8cf59c6 --- /dev/null +++ b/auth-helper-dropbox.ts @@ -0,0 +1,33 @@ +/// + +import * as tnsOauth from './tns-oauth'; +import { AuthHelper } from './auth-helper'; +import * as TnsOAuth from './tns-oauth-interfaces'; + +/** + Contains DropBox connection credentials +*/ +export class AuthHelperDropBox extends AuthHelper implements TnsOAuth.ITnsAuthHelper { + //Constructs the the object with specified id, secret and scope + constructor(clientId: string, clientSecret: string, redirectUri: string, scope: Array) { + super(); + var scopeStr = scope.join('%20'); + this.credentials = { + authority: 'https://www.dropbox.com', + tokenEndpointBase: 'https://api.dropboxapi.com', + authorizeEndpoint: '/oauth2/authorize', + tokenEndpoint: '/oauth2/token', + clientId: clientId, + clientSecret: clientSecret, + redirectUri: redirectUri, + scope: scopeStr, + ignoredAuthParams: ['response_mode','nonce'], + ignoredTokenParams: ['response_mode','nonce','state'] + }; + } + //Gets cookie domains for logging out + public logout(successPage?: string): Promise { + let cookieDomains = [".dropbox.com"]; + return this._logout(successPage, cookieDomains); + } +} diff --git a/demo-angular/app/main.ts b/demo-angular/app/main.ts index d553a07..131315a 100644 --- a/demo-angular/app/main.ts +++ b/demo-angular/app/main.ts @@ -24,6 +24,14 @@ var facebookInitOptions: tnsOAuthModule.ITnsOAuthOptionsFacebook = { tnsOAuthModule.initFacebook(facebookInitOptions); +// var dropboxInitOptions: tnsOAuthModule.ITnsOAuthOptionsDropBox = { +// clientId: 'XXXXXXXXXX', +// clientSecret: 'XXXXXXXXXX', +// redirectUri: 'http://localhost', +// scope: [], +// }; + +// tnsOAuthModule.initDropBox(dropboxInitOptions); // let uaaInitOptions: tnsOAuthModule.ITnsOAuthOptionsUaa = { diff --git a/index.d.ts b/index.d.ts index a7e9935..b36d80a 100644 --- a/index.d.ts +++ b/index.d.ts @@ -9,6 +9,7 @@ export declare function initGoogle(options: TnsOAuth.ITnsOAuthOptionsGoogle): Pr export declare function initUaa(options: TnsOAuth.ITnsOAuthOptionsUaa): Promise; export declare function initLinkedIn(options: TnsOAuth.ITnsOAuthOptionsLinkedIn): Promise; export declare function initSalesforce(options: TnsOAuth.ITnsOAuthOptionsSalesforce): Promise; +export declare function initDropBox(options: TnsOAuth.ITnsOAuthOptionsDropBox): Promise; export declare function accessToken(): string; export declare function login(successPage?: string): Promise; diff --git a/index.ts b/index.ts index 06b48a5..c0cc9ea 100644 --- a/index.ts +++ b/index.ts @@ -10,6 +10,7 @@ import { AuthHelperGoogle } from './auth-helper-google'; import { AuthHelperUaa } from './auth-helper-uaa'; import { AuthHelperLinkedIn } from './auth-helper-linkedin'; import { AuthHelperSalesforce } from './auth-helper-salesforce'; +import { AuthHelperDropBox } from './auth-helper-dropbox'; import * as TnsOAuth from './tns-oauth-interfaces'; @@ -125,6 +126,23 @@ export function initSalesforce(options: TnsOAuth.ITnsOAuthOptionsSalesforce): Pr }); } +export function initDropBox(options: TnsOAuth.ITnsOAuthOptionsDropBox): Promise { + return new Promise(function (resolve, reject) { + try { + if (instance !== null) { + reject("You already ran init"); + return; + } + + instance = new AuthHelperDropBox(options.clientId, options.clientSecret, options.redirectUri, options.scope); + resolve(instance); + } catch (ex) { + console.log("Error in AuthHelperLinkedIn.init: " + ex); + reject(ex); + } + }); +} + diff --git a/tns-oauth-interfaces.d.ts b/tns-oauth-interfaces.d.ts index 3175248..17b4838 100644 --- a/tns-oauth-interfaces.d.ts +++ b/tns-oauth-interfaces.d.ts @@ -22,6 +22,8 @@ export interface ITnsOAuthCredentials { redirectUri: string; responseType?: string; scope: string; + ignoredTokenParams?: string[]; + ignoredAuthParams?: string[]; } export interface ITnsOAuthCredentialsUaa extends ITnsOAuthCredentials { @@ -68,3 +70,8 @@ export interface ITnsOAuthOptionsSalesforce extends ITnsOAuthOptions { redirectUri: string; responseType: string; } + +export interface ITnsOAuthOptionsDropBox extends ITnsOAuthOptions { + clientSecret: string; + redirectUri: string; +} diff --git a/tns-oauth.ts b/tns-oauth.ts index 2ab7a75..89247d5 100644 --- a/tns-oauth.ts +++ b/tns-oauth.ts @@ -25,6 +25,20 @@ function getAuthHeaderFromCredentials(credentials: TnsOAuthModule.ITnsOAuthCrede return customAuthHeader; } +function isIgnoredParameter(paramName: string, ignoredParameters: string[]): boolean { + return ignoredParameters.indexOf(paramName) > -1; +} + +function setMemberIfNotIgnored(params: any, paramName: string, ignoredParams: string[], value: string): any { + if(!isIgnoredParameter(paramName, ignoredParams)) params[paramName] = value; + return params; +} + +function addQueryStringIfNotIgnored(queryStr: string, paramName: string, ignoredParams: string[], value: string): string { + if(!isIgnoredParameter(paramName, ignoredParams)) queryStr += (queryStr.startsWith('?') ? '&' : '?') + (paramName + '=' + value); + return queryStr; +} + /** * Gets a token for a given resource. @@ -43,13 +57,15 @@ function getTokenFromCode(credentials: TnsOAuthModule.ITnsOAuthCredentials, code customAuthHeader ); - let oauthParams = { - grant_type: 'authorization_code', - redirect_uri: credentials.redirectUri, - response_mode: 'form_post', - nonce: utils.newUUID(), - state: 'abcd' - }; + credentials.ignoredTokenParams = credentials.ignoredTokenParams || []; + + let oauthParams = { }; + + oauthParams = setMemberIfNotIgnored(oauthParams, 'grant_type', credentials.ignoredTokenParams, 'authorization_code'); + oauthParams = setMemberIfNotIgnored(oauthParams, 'redirect_uri', credentials.ignoredTokenParams, credentials.redirectUri); + oauthParams = setMemberIfNotIgnored(oauthParams, 'response_mode', credentials.ignoredTokenParams, 'form_post'); + oauthParams = setMemberIfNotIgnored(oauthParams, 'nonce', credentials.ignoredTokenParams, utils.newUUID()); + oauthParams = setMemberIfNotIgnored(oauthParams, 'state', credentials.ignoredTokenParams, 'abcd'); return oauth2.getOAuthAccessToken(code, oauthParams); } @@ -71,13 +87,15 @@ export function getTokenFromRefreshToken(credentials: TnsOAuthModule.ITnsOAuthCr customAuthHeader ); - let oauthParams = { - grant_type: 'refresh_token', - redirect_uri: credentials.redirectUri, - response_mode: 'form_post', - nonce: utils.newUUID(), - state: 'abcd' - }; + credentials.ignoredTokenParams = credentials.ignoredTokenParams || []; + + let oauthParams = { }; + + oauthParams = setMemberIfNotIgnored(oauthParams, 'grant_type', credentials.ignoredTokenParams, 'refresh_token'); + oauthParams = setMemberIfNotIgnored(oauthParams, 'redirect_uri', credentials.ignoredTokenParams, credentials.redirectUri); + oauthParams = setMemberIfNotIgnored(oauthParams, 'response_mode', credentials.ignoredTokenParams, 'form_post'); + oauthParams = setMemberIfNotIgnored(oauthParams, 'nonce', credentials.ignoredTokenParams, utils.newUUID()); + oauthParams = setMemberIfNotIgnored(oauthParams, 'state', credentials.ignoredTokenParams, 'abcd'); return oauth2.getOAuthAccessToken(refreshToken, oauthParams); } @@ -87,14 +105,19 @@ export function getTokenFromRefreshToken(credentials: TnsOAuthModule.ITnsOAuthCr * @return {string} a fully formed uri with which authentication can be completed */ export function getAuthUrl(credentials: TnsOAuthModule.ITnsOAuthCredentials): string { - return credentials.authority + credentials.authorizeEndpoint + - '?client_id=' + credentials.clientId + - '&response_type=code' + - '&redirect_uri=' + credentials.redirectUri + - '&scope=' + credentials.scope + - '&response_mode=query' + - '&nonce=' + utils.newUUID() + - '&state=abcd'; + var queryStr = ''; + + credentials.ignoredAuthParams = credentials.ignoredAuthParams || []; + + queryStr = addQueryStringIfNotIgnored(queryStr, 'client_id', credentials.ignoredAuthParams, credentials.clientId); + queryStr = addQueryStringIfNotIgnored(queryStr, 'response_type', credentials.ignoredAuthParams, 'code'); + queryStr = addQueryStringIfNotIgnored(queryStr, 'redirect_uri', credentials.ignoredAuthParams, credentials.redirectUri); + queryStr = addQueryStringIfNotIgnored(queryStr, 'scope', credentials.ignoredAuthParams, credentials.scope); + queryStr = addQueryStringIfNotIgnored(queryStr, 'response_mode', credentials.ignoredAuthParams, 'query'); + queryStr = addQueryStringIfNotIgnored(queryStr, 'nonce', credentials.ignoredAuthParams, utils.newUUID()); + queryStr = addQueryStringIfNotIgnored(queryStr, 'state', credentials.ignoredAuthParams, 'abcd'); + + return credentials.authority + credentials.authorizeEndpoint + queryStr; } export function getTokenFromCache() { diff --git a/tsconfig.json b/tsconfig.json index ad6ff72..83e5d73 100755 --- a/tsconfig.json +++ b/tsconfig.json @@ -29,6 +29,7 @@ "auth-helper-office365.ts", "auth-helper-uaa.ts", "auth-helper-linkedin.ts", + "auth-helper-dropbox.ts", "index.ts", "tns-oauth.ts", "tns-oauth-page-provider.ts", From cb66efff7efd4e41e96968d240331d8f79173672 Mon Sep 17 00:00:00 2001 From: Jason McNair Date: Wed, 17 Jan 2018 10:36:32 -0600 Subject: [PATCH 2/2] fix for subsequent calls to getTokenFromCode being rejected by Dropbox --- auth-helper-dropbox.ts | 3 ++- tns-oauth-interfaces.d.ts | 1 + tns-oauth.ts | 4 ++++ 3 files changed, 7 insertions(+), 1 deletion(-) diff --git a/auth-helper-dropbox.ts b/auth-helper-dropbox.ts index 8cf59c6..3f3f4b0 100644 --- a/auth-helper-dropbox.ts +++ b/auth-helper-dropbox.ts @@ -22,7 +22,8 @@ export class AuthHelperDropBox extends AuthHelper implements TnsOAuth.ITnsAuthHe redirectUri: redirectUri, scope: scopeStr, ignoredAuthParams: ['response_mode','nonce'], - ignoredTokenParams: ['response_mode','nonce','state'] + ignoredTokenParams: ['response_mode','nonce','state'], + skipNextTokenNav: true }; } //Gets cookie domains for logging out diff --git a/tns-oauth-interfaces.d.ts b/tns-oauth-interfaces.d.ts index 17b4838..b43a164 100644 --- a/tns-oauth-interfaces.d.ts +++ b/tns-oauth-interfaces.d.ts @@ -24,6 +24,7 @@ export interface ITnsOAuthCredentials { scope: string; ignoredTokenParams?: string[]; ignoredAuthParams?: string[]; + skipNextTokenNav?: boolean; } export interface ITnsOAuthCredentialsUaa extends ITnsOAuthCredentials { diff --git a/tns-oauth.ts b/tns-oauth.ts index 89247d5..ff6f84a 100644 --- a/tns-oauth.ts +++ b/tns-oauth.ts @@ -127,6 +127,7 @@ export function getTokenFromCache() { export function loginViaAuthorizationCodeFlow(credentials: TnsOAuthModule.ITnsOAuthCredentials, successPage?: string): Promise { return new Promise((resolve, reject) => { var navCount = 0; + var tokenNavStarted = false; let checkCodeIntercept = (webView, error, url): boolean => { var retStr = ''; @@ -158,6 +159,9 @@ export function loginViaAuthorizationCodeFlow(credentials: TnsOAuthModule.ITnsOA let errSubCode = qsObj['error_subcode']; if (codeStr) { try { + if(credentials.skipNextTokenNav && tokenNavStarted) return true; + + tokenNavStarted = true; getTokenFromCode(credentials, codeStr) .then((response: TnsOAuthModule.ITnsOAuthTokenResult) => { TnsOAuthTokenCache.setToken(response);