Skip to content

Commit

Permalink
Feat/oauth1 support (NangoHQ#43)
Browse files Browse the repository at this point in the history
* fix: stronger typing to prevent typos

* chore: fix integration configs

* chore: imporve typing and rename config to auth

* chore: prepare ofr oauth1 token refresh

* chore: minor refactoring and bug fix

* feat: make OAuth1 request work

* feat: better error handling

* chore: remove log statement

* feat: add extra variables

allow request signing

* fix: feedback

Co-authored-by: Corentin Brossault <[email protected]>
  • Loading branch information
tanguyantoine and Frenchcooc authored Jun 2, 2020
1 parent c0d368d commit 16c5453
Show file tree
Hide file tree
Showing 22 changed files with 221 additions and 127 deletions.
3 changes: 1 addition & 2 deletions integrations/moltin.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,7 @@
"config": {
"authType": "OAUTH2",
"bodyFormat": "form",
"grantType": "client_credentials",
"tokenParams": { "grantType": "client_credentials" },
"tokenParams": { "grant_type": "client_credentials" },
"authorizationMethod": "body",
"callbackURL": "https://int.bearer.sh/v2/auth/callback",
"tokenURL": "https://api.moltin.com/oauth/access_token"
Expand Down
3 changes: 1 addition & 2 deletions integrations/snov.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,8 @@
"config": {
"authType": "OAUTH2",
"bodyFormat": "form",
"grantType": "client_credentials",
"config": { "scope": [] },
"tokenParams": {},
"tokenParams": { "grant_type": "client_credentials" },
"tokenURL": "https://app.snov.io/oauth/access_token",
"authorizationMethod": "body",
"authorizationParams": {}
Expand Down
3 changes: 1 addition & 2 deletions integrations/twitch.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,8 @@
"config": {
"authType": "OAUTH2",
"bodyFormat": "form",
"grantType": "authorization_code",
"config": { "scope": ["user:read:email"] },
"tokenParams": {},
"tokenParams": { "grant_type": "authorization_code" },
"authorizationMethod": "body",
"authorizationParams": { "redirectUri": "https://int.bearer.sh/v2/auth/callback", "responseType": "code" },
"authorizationURL": "https://id.twitch.tv/oauth2/authorize",
Expand Down
14 changes: 7 additions & 7 deletions integrations/xero.json
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
{
"name": "Xero",
"config": {
"requestTokenURL": "https://api.xero.com/oauth/RequestToken",
"accessTokenURL": "https://api.xero.com/oauth/AccessToken",
"userAuthorizationURL": "https://api.xero.com/oauth/Authorize",
"callbackURL": "https://int.bearer.sh/v2/auth/callback",
"authType": "OAUTH1",
"signatureMethod": "HMAC-SHA1",
"tokenParams": {},
"authorizationParams": {},
"authType": "OAUTH1",
"callbackURL": "https://int.bearer.sh/v2/auth/callback",
"config": { "scope": [] },
"hint": "You will find your Xero credentials here: https://developer.xero.com/myapps",
"provider": "Xero",
"hint": "You will find your Xero credentials here: https://developer.xero.com/myapps"
"requestTokenURL": "https://api.xero.com/oauth/RequestToken",
"signatureMethod": "HMAC-SHA1",
"tokenParams": {},
"userAuthorizationURL": "https://api.xero.com/oauth/Authorize"
},
"request": {
"baseURL": "https://api.xero.com/api.xro/2.0/",
Expand Down
2 changes: 1 addition & 1 deletion src/legacy/api-config/request-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ const additionalAuthVariables = ({ auth, authType, baseURL, method, path }: Addi
}
}

const getOAuth1Credentials = ({ baseURL, method, path, auth }: IGetOAuth1CredentialsParams) => {
export const getOAuth1Credentials = ({ baseURL, method, path, auth }: IGetOAuth1CredentialsParams) => {
const { consumerKey, consumerSecret, accessToken, tokenSecret, signatureMethod } = auth

const absUrl = `${baseURL.replace(/\/+$/, '')}/${path.replace(/^\/+/, '')}`
Expand Down
2 changes: 1 addition & 1 deletion src/legacy/auth/clients/oauth2.ts
Original file line number Diff line number Diff line change
Expand Up @@ -228,7 +228,7 @@ interface CodeParams extends TokenClientParams {
}

interface RefreshParams extends TokenClientParams {
idToken: string
idToken?: string
refreshToken: string
}

Expand Down
2 changes: 0 additions & 2 deletions src/legacy/auth/v3/error-handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,11 +53,9 @@ export const errorHandler = (err: any, req: TErrorHandlerRequest, res: Response,

return respondWithOAuthError(res, err)
}

respondWithOAuthError(res, {
statusCode: 500,
code: 'INTERNAL_ERROR',
message: 'Encountered an unexpected error. Please contact support'
})
next(err)
}
4 changes: 1 addition & 3 deletions src/legacy/auth/v3/strategies/oauth1.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ class Strategy extends OAuth1Strategy {

const strategyOptions = (req: TAuthenticateRequest) => {
const callbackURL = process.env.AUTH_CALLBACK_URL || `${req.protocol}://${req.get('host')}/auth/callback`
const { consumerKey, consumerSecret } = req.setupDetails
const { consumerKey, consumerSecret } = req.setupDetails.credentials
const {
requestTokenURL,
tokenParams,
Expand All @@ -88,7 +88,6 @@ const strategyOptions = (req: TAuthenticateRequest) => {
authorizationParams,
signatureMethod
} = req.integrationConfig

return {
consumerKey,
consumerSecret,
Expand Down Expand Up @@ -123,7 +122,6 @@ export const authenticate = (req: TAuthenticateRequest, res: Response, next: Nex
if (!accessToken) {
return verified(undefined, undefined, { message: 'No access token returned', response: params })
}

const { consumerKey, consumerSecret } = req.setupDetails

const credentials: IOAuth1Credentials = {
Expand Down
1 change: 0 additions & 1 deletion src/legacy/auth/v3/strategy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ export const isOAuthType = (authType: EAuthType) => [EAuthType.OAuth1, EAuthType

export const authenticate = (req: TAuthenticateRequest, res: Response, next: NextFunction) => {
const { authType } = req.integrationConfig

strategies[authType].authenticate(req, res, next)
}

Expand Down
2 changes: 1 addition & 1 deletion src/legacy/auth/v3/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -187,7 +187,7 @@ export interface OAuth1AuthDetails extends OAuthAuthDetails {
tokenSecret: string
consumerKey: string
consumerSecret: string
signatureMethod: OAuth1SignatureMethod
signatureMethod: 'HMAC-SHA1' | 'PLAINTEXT' | 'RSA-SHA1'
}

export interface OAuth2AuthDetails extends OAuthAuthDetails {
Expand Down
27 changes: 21 additions & 6 deletions src/lib/database/integrations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,10 +69,13 @@ const formatIntegration = (fileName: string, fileContent: any) => {
integration.id = fileName
integration.image =
integration.image || 'http://logo.clearbit.com/' + integration.name.toLowerCase().replace(' ', '') + '.com'
// Hack to prevent mix match things
// @ts-expect-error
integration.auth = integration.config

const isOAuth2 = integration.config.authType === 'OAUTH2'
integration.config.setupKeyLabel = isOAuth2 ? 'Client ID' : 'Consumer Key'
integration.config.setupSecretLabel = isOAuth2 ? 'Client Secret' : 'Consumer Secret'
const isOAuth2Auth = isOAuth2(integration)
integration.auth.setupKeyLabel = isOAuth2Auth ? 'Client ID' : 'Consumer Key'
integration.auth.setupSecretLabel = isOAuth2Auth ? 'Client Secret' : 'Consumer Secret'

return integration
}
Expand All @@ -95,9 +98,9 @@ export const validateConfigurationCredentials = (
return
}

const integrationConfig = integration.config
const isOAuth2 = integrationConfig.authType == 'OAUTH2'
const isOAuth1 = integrationConfig.authType == 'OAUTH1'
const authConfig = integration.auth
const isOAuth2 = authConfig.authType == 'OAUTH2'
const isOAuth1 = authConfig.authType == 'OAUTH1'

if (isOAuth1) {
const consumerKey = String(setup.consumerKey)
Expand All @@ -117,3 +120,15 @@ export const validateConfigurationCredentials = (

return
}

/*
Helpers
*/

export function isOAuth2(integration: Types.Integration): integration is Types.Integration<Types.OAuth2Config> {
return integration.auth.authType === 'OAUTH2'
}

export function isOAuth1(integration: Types.Integration): integration is Types.Integration<Types.OAuth1Config> {
return integration.auth.authType === 'OAUTH1'
}
6 changes: 6 additions & 0 deletions src/lib/error-handling/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,12 @@ export class PizzlyError extends Error {
this.message = 'Scopes are malformed. Must be in the form string[].'
break

// Something failed
case 'token_refresh_failed':
this.status = 422
this.message = 'Unable to refresh the token. Please re-connect that user.'
break

// General case for unhandled errors
default:
this.status = 500
Expand Down
32 changes: 17 additions & 15 deletions src/lib/oauth/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import * as OAuth2 from './oauth2'
import { Types } from '../../types'
import { configurations, authentications } from '../database'
import { isOAuth2 } from '../database/integrations'

/**
* Determine if an access token has expired by comparing
Expand All @@ -11,13 +12,12 @@ import { configurations, authentications } from '../database'
*/

export const accessTokenHasExpired = async (authentication: Types.Authentication) => {
const payload = authentication.payload
const { expiresIn } = authentication.payload

if (!payload.expiresIn) {
if (!expiresIn) {
return false
}

const expiresIn = payload.expiresIn
const updatedAt = Date.parse(authentication.updated_at)
const expiredFromThisTime = expiresIn * 1000 + updatedAt
const safeRefreshTime = expiredFromThisTime - 15 * 60 * 1000 // 15 minutes
Expand All @@ -40,17 +40,19 @@ export const refreshAuthentication = async (
oldAuthentication: Types.Authentication
) => {
const configuration = await configurations.get(integration.id, oldAuthentication.setup_id)
const newPayload = await OAuth2.refresh(integration, configuration, oldAuthentication)

const newAuthentication: Types.Authentication = {
auth_id: oldAuthentication.auth_id,
setup_id: oldAuthentication.setup_id,
payload: newPayload,
created_at: oldAuthentication.created_at,
updated_at: new Date().toISOString()
if (isOAuth2(integration)) {
const newPayload = await OAuth2.refresh(integration, configuration, oldAuthentication)

const newAuthentication: Types.Authentication = {
auth_id: oldAuthentication.auth_id,
setup_id: oldAuthentication.setup_id,
payload: newPayload,
created_at: oldAuthentication.created_at,
updated_at: new Date().toISOString()
}

await authentications.update(oldAuthentication.auth_id, newAuthentication)
return newAuthentication
}

await authentications.update(oldAuthentication.auth_id, newAuthentication)

return newAuthentication
// TODO handle oauth1 token freshness
}
15 changes: 6 additions & 9 deletions src/lib/oauth/oauth2/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import { Types } from '../../../types'
*/

export const refresh = async (
integration: Types.Integration,
integration: Types.Integration<Types.OAuth2Config>,
configuration: Types.Configuration,
oldAuthentication: Types.Authentication
): Promise<Types.OAuth2Payload> => {
Expand All @@ -21,22 +21,19 @@ export const refresh = async (
const { clientId, clientSecret } = configuration.credentials as Types.OAuth2Credentials

if (!refreshToken) {
const { grantType } = integration.config

const { tokenParams, tokenURL, authorizationMethod, bodyFormat } = integration.auth
const { grant_type: grantType } = tokenParams
if (grantType !== GrantType.ClientCredentials) {
throw new AccessTokenExpired()
}

const scope = configuration.scopes
const { authorizationMethod, bodyFormat, tokenURL } = integration.config

const tokenResult = await getTokenWithClientCredentials({
authorizationMethod,
bodyFormat,
clientId,
clientSecret,
scope,
tokenURL
tokenURL,
scope: configuration.scopes
})

const oauthPayload: Types.OAuth2Payload = {
Expand All @@ -50,7 +47,7 @@ export const refresh = async (
return oauthPayload
}

const { idToken, refreshURL, tokenURL } = integration.config
const { idToken, refreshURL, tokenURL } = integration.auth

const tokenResult = await getTokenWithRefreshToken({
...{ clientId, clientSecret },
Expand Down
Loading

0 comments on commit 16c5453

Please sign in to comment.