diff --git a/README.md b/README.md index cc866151..f0864769 100644 --- a/README.md +++ b/README.md @@ -254,6 +254,57 @@ auth0.auth .catch(console.error); ``` +#### Login with Passwordless + +Passwordless is a two-step authentication flow that makes use of this type of connection. The **Passwordless OTP** grant is required to be enabled in your Auth0 application beforehand. Check [our guide](https://auth0.com/docs/dashboard/guides/applications/update-grant-types) to learn how to enable it. + +To start the flow, you request a code to be sent to the user's email or phone number. For email scenarios only, a link can be sent in place of the code. + +```js +auth0.auth + .passwordlessWithEmail({ + email: 'info@auth0.com', + send: 'link', + }) + .then(console.log) + .catch(console.error); +``` + +or + +```js +auth0.auth + .passwordlessWithSMS({ + phoneNumber: '+5491159991000', + }) + .then(console.log) + .catch(console.error); +``` + +Then, in order to complete the authentication, you must send back that received code value along with the email or phone number used: + +```js +auth0.auth + .loginWithEmail({ + email: 'info@auth0.com', + code: '123456', + }) + .then(console.log) + .catch(console.error); +``` + +or + +```js +auth0.auth + .loginWithSMS({ + phoneNumber: 'info@auth0.com', + code: '123456', + }) + .then(console.log) + .catch(console.error); +``` + #### Create user in database connection ```js diff --git a/src/auth/__tests__/__snapshots__/index.spec.js.snap b/src/auth/__tests__/__snapshots__/index.spec.js.snap index fe00a094..e8b4a1b5 100644 --- a/src/auth/__tests__/__snapshots__/index.spec.js.snap +++ b/src/auth/__tests__/__snapshots__/index.spec.js.snap @@ -149,6 +149,156 @@ Array [ ] `; +exports[`auth passwordless flow with SMS connection should begin with code 1`] = ` +Array [ + "https://samples.auth0.com/passwordless/start", + Object { + "body": "{\\"phone_number\\":\\"+5491159991000\\",\\"send\\":\\"code\\",\\"connection\\":\\"sms\\",\\"client_id\\":\\"A_CLIENT_ID_OF_YOUR_ACCOUNT\\"}", + "headers": Object { + "Accept": "application/json", + "Auth0-Client": "eyJuYW1lIjoicmVhY3QtbmF0aXZlLWF1dGgwIiwidmVyc2lvbiI6IjEuMC4wIn0=", + "Content-Type": "application/json", + }, + "method": "POST", + }, +] +`; + +exports[`auth passwordless flow with SMS connection should begin with link 1`] = ` +Array [ + "https://samples.auth0.com/passwordless/start", + Object { + "body": "{\\"phone_number\\":\\"+5491159991000\\",\\"send\\":\\"link\\",\\"connection\\":\\"sms\\",\\"client_id\\":\\"A_CLIENT_ID_OF_YOUR_ACCOUNT\\"}", + "headers": Object { + "Accept": "application/json", + "Auth0-Client": "eyJuYW1lIjoicmVhY3QtbmF0aXZlLWF1dGgwIiwidmVyc2lvbiI6IjEuMC4wIn0=", + "Content-Type": "application/json", + }, + "method": "POST", + }, +] +`; + +exports[`auth passwordless flow with SMS connection should begin with optional parameters 1`] = ` +Array [ + "https://samples.auth0.com/passwordless/start", + Object { + "body": "{\\"phone_number\\":\\"+5491159991000\\",\\"send\\":\\"code\\",\\"authParams\\":{\\"scope\\":\\"openid profile\\"},\\"connection\\":\\"sms\\",\\"client_id\\":\\"A_CLIENT_ID_OF_YOUR_ACCOUNT\\"}", + "headers": Object { + "Accept": "application/json", + "Auth0-Client": "eyJuYW1lIjoicmVhY3QtbmF0aXZlLWF1dGgwIiwidmVyc2lvbiI6IjEuMC4wIn0=", + "Content-Type": "application/json", + }, + "method": "POST", + }, +] +`; + +exports[`auth passwordless flow with SMS connection should continue 1`] = ` +Array [ + "https://samples.auth0.com/oauth/token", + Object { + "body": "{\\"username\\":\\"+5491159991000\\",\\"otp\\":\\"123456\\",\\"client_id\\":\\"A_CLIENT_ID_OF_YOUR_ACCOUNT\\",\\"realm\\":\\"sms\\",\\"grant_type\\":\\"http://auth0.com/oauth/grant-type/passwordless/otp\\"}", + "headers": Object { + "Accept": "application/json", + "Auth0-Client": "eyJuYW1lIjoicmVhY3QtbmF0aXZlLWF1dGgwIiwidmVyc2lvbiI6IjEuMC4wIn0=", + "Content-Type": "application/json", + }, + "method": "POST", + }, +] +`; + +exports[`auth passwordless flow with SMS connection should continue with optional parameters 1`] = ` +Array [ + "https://samples.auth0.com/oauth/token", + Object { + "body": "{\\"username\\":\\"+5491159991000\\",\\"otp\\":\\"123456\\",\\"audience\\":\\"http://myapi.com\\",\\"scope\\":\\"openid\\",\\"client_id\\":\\"A_CLIENT_ID_OF_YOUR_ACCOUNT\\",\\"realm\\":\\"sms\\",\\"grant_type\\":\\"http://auth0.com/oauth/grant-type/passwordless/otp\\"}", + "headers": Object { + "Accept": "application/json", + "Auth0-Client": "eyJuYW1lIjoicmVhY3QtbmF0aXZlLWF1dGgwIiwidmVyc2lvbiI6IjEuMC4wIn0=", + "Content-Type": "application/json", + }, + "method": "POST", + }, +] +`; + +exports[`auth passwordless flow with email connection should begin with code 1`] = ` +Array [ + "https://samples.auth0.com/passwordless/start", + Object { + "body": "{\\"email\\":\\"info@auth0.com\\",\\"send\\":\\"link\\",\\"connection\\":\\"email\\",\\"client_id\\":\\"A_CLIENT_ID_OF_YOUR_ACCOUNT\\"}", + "headers": Object { + "Accept": "application/json", + "Auth0-Client": "eyJuYW1lIjoicmVhY3QtbmF0aXZlLWF1dGgwIiwidmVyc2lvbiI6IjEuMC4wIn0=", + "Content-Type": "application/json", + }, + "method": "POST", + }, +] +`; + +exports[`auth passwordless flow with email connection should begin with link 1`] = ` +Array [ + "https://samples.auth0.com/passwordless/start", + Object { + "body": "{\\"email\\":\\"info@auth0.com\\",\\"send\\":\\"link\\",\\"connection\\":\\"email\\",\\"client_id\\":\\"A_CLIENT_ID_OF_YOUR_ACCOUNT\\"}", + "headers": Object { + "Accept": "application/json", + "Auth0-Client": "eyJuYW1lIjoicmVhY3QtbmF0aXZlLWF1dGgwIiwidmVyc2lvbiI6IjEuMC4wIn0=", + "Content-Type": "application/json", + }, + "method": "POST", + }, +] +`; + +exports[`auth passwordless flow with email connection should begin with optional parameters 1`] = ` +Array [ + "https://samples.auth0.com/passwordless/start", + Object { + "body": "{\\"email\\":\\"info@auth0.com\\",\\"send\\":\\"code\\",\\"authParams\\":{\\"scope\\":\\"openid profile\\"},\\"connection\\":\\"email\\",\\"client_id\\":\\"A_CLIENT_ID_OF_YOUR_ACCOUNT\\"}", + "headers": Object { + "Accept": "application/json", + "Auth0-Client": "eyJuYW1lIjoicmVhY3QtbmF0aXZlLWF1dGgwIiwidmVyc2lvbiI6IjEuMC4wIn0=", + "Content-Type": "application/json", + }, + "method": "POST", + }, +] +`; + +exports[`auth passwordless flow with email connection should continue 1`] = ` +Array [ + "https://samples.auth0.com/oauth/token", + Object { + "body": "{\\"username\\":\\"info@auth0.com\\",\\"otp\\":\\"123456\\",\\"client_id\\":\\"A_CLIENT_ID_OF_YOUR_ACCOUNT\\",\\"realm\\":\\"email\\",\\"grant_type\\":\\"http://auth0.com/oauth/grant-type/passwordless/otp\\"}", + "headers": Object { + "Accept": "application/json", + "Auth0-Client": "eyJuYW1lIjoicmVhY3QtbmF0aXZlLWF1dGgwIiwidmVyc2lvbiI6IjEuMC4wIn0=", + "Content-Type": "application/json", + }, + "method": "POST", + }, +] +`; + +exports[`auth passwordless flow with email connection should continue with optional parameters 1`] = ` +Array [ + "https://samples.auth0.com/oauth/token", + Object { + "body": "{\\"username\\":\\"info@auth0.com\\",\\"otp\\":\\"123456\\",\\"audience\\":\\"http://myapi.com\\",\\"scope\\":\\"openid\\",\\"client_id\\":\\"A_CLIENT_ID_OF_YOUR_ACCOUNT\\",\\"realm\\":\\"email\\",\\"grant_type\\":\\"http://auth0.com/oauth/grant-type/passwordless/otp\\"}", + "headers": Object { + "Accept": "application/json", + "Auth0-Client": "eyJuYW1lIjoicmVhY3QtbmF0aXZlLWF1dGgwIiwidmVyc2lvbiI6IjEuMC4wIn0=", + "Content-Type": "application/json", + }, + "method": "POST", + }, +] +`; + exports[`auth refresh token should handle oauth error 1`] = `[invalid_request: Invalid grant]`; exports[`auth refresh token should handle unexpected error 1`] = `[a0.response.invalid: Internal Server Error]`; diff --git a/src/auth/__tests__/index.spec.js b/src/auth/__tests__/index.spec.js index 2cf98a87..b03119ab 100644 --- a/src/auth/__tests__/index.spec.js +++ b/src/auth/__tests__/index.spec.js @@ -4,9 +4,14 @@ import fetchMock from 'fetch-mock'; describe('auth', () => { const baseUrl = 'samples.auth0.com'; const clientId = 'A_CLIENT_ID_OF_YOUR_ACCOUNT'; - const telemetry = { name: 'react-native-auth0', version: '1.0.0' }; + const telemetry = {name: 'react-native-auth0', version: '1.0.0'}; const redirectUri = 'https://mysite.com/callback'; const state = 'a random state for auth'; + const emptySuccess = { + status: 200, + body: {}, + headers: {'Content-Type': 'application/json'}, + }; const tokens = { status: 200, body: { @@ -14,39 +19,39 @@ describe('auth', () => { id_token: 'an id token', expires_in: 1234567890, state, - scope: 'openid' + scope: 'openid', }, - headers: { 'Content-Type': 'application/json' } + headers: {'Content-Type': 'application/json'}, }; const oauthError = { status: 400, body: { error: 'invalid_request', - error_description: 'Invalid grant' + error_description: 'Invalid grant', }, - headers: { 'Content-Type': 'application/json' } + headers: {'Content-Type': 'application/json'}, }; const unexpectedError = { status: 500, body: 'Internal Server Error....', - headers: { 'Content-Type': 'text/plain' } + headers: {'Content-Type': 'text/plain'}, }; - const auth = new Auth({ baseUrl, clientId, telemetry }); + const auth = new Auth({baseUrl, clientId, telemetry}); beforeEach(fetchMock.restore); describe('constructor', () => { it('should build with domain', () => { - const auth = new Auth({ baseUrl, clientId }); + const auth = new Auth({baseUrl, clientId}); expect(auth.clientId).toEqual(clientId); }); it('should fail without clientId', () => { - expect(() => new Auth({ baseUrl })).toThrowErrorMatchingSnapshot(); + expect(() => new Auth({baseUrl})).toThrowErrorMatchingSnapshot(); }); it('should fail without domain', () => { - expect(() => new Auth({ clientId })).toThrowErrorMatchingSnapshot(); + expect(() => new Auth({clientId})).toThrowErrorMatchingSnapshot(); }); }); @@ -56,8 +61,8 @@ describe('auth', () => { auth.authorizeUrl({ responseType: 'code', redirectUri, - state: 'a_random_state' - }) + state: 'a_random_state', + }), ).toMatchSnapshot(); }); @@ -67,8 +72,8 @@ describe('auth', () => { responseType: 'code', redirectUri, state: 'a_random_state', - connection: 'facebook' - }) + connection: 'facebook', + }), ).toMatchSnapshot(); }); }); @@ -83,8 +88,8 @@ describe('auth', () => { auth.logoutUrl({ federated: true, clientId: 'CLIENT_ID', - redirectTo: 'https://auth0.com' - }) + redirectTo: 'https://auth0.com', + }), ).toMatchSnapshot(); }); @@ -92,8 +97,8 @@ describe('auth', () => { expect( auth.logoutUrl({ federated: true, - shouldNotBeThere: 'really' - }) + shouldNotBeThere: 'really', + }), ).toMatchSnapshot(); }); }); @@ -107,7 +112,7 @@ describe('auth', () => { verifier: 'a verifier', redirectUri, state, - scope: 'openid' + scope: 'openid', }); expect(fetchMock.lastCall()).toMatchSnapshot(); }); @@ -120,7 +125,7 @@ describe('auth', () => { verifier: 'a verifier', redirectUri, state, - scope: 'openid' + scope: 'openid', }; await expect(auth.exchange(parameters)).resolves.toMatchSnapshot(); }); @@ -133,7 +138,7 @@ describe('auth', () => { verifier: 'a verifier', redirectUri, state, - scope: 'openid' + scope: 'openid', }; await expect(auth.exchange(parameters)).rejects.toMatchSnapshot(); }); @@ -141,7 +146,7 @@ describe('auth', () => { it('should handle unexpected error', async () => { fetchMock.postOnce( 'https://samples.auth0.com/oauth/token', - unexpectedError + unexpectedError, ); expect.assertions(1); const parameters = { @@ -149,19 +154,153 @@ describe('auth', () => { verifier: 'a verifier', redirectUri, state, - scope: 'openid' + scope: 'openid', }; await expect(auth.exchange(parameters)).rejects.toMatchSnapshot(); }); }); + describe('passwordless flow', () => { + describe('with email connection', () => { + it('should begin with code', async () => { + fetchMock.postOnce( + 'https://samples.auth0.com/passwordless/start', + emptySuccess, + ); + expect.assertions(1); + await auth.passwordlessWithEmail({ + email: 'info@auth0.com', + send: 'link', + }); + expect(fetchMock.lastCall()).toMatchSnapshot(); + }); + + it('should begin with link', async () => { + fetchMock.postOnce( + 'https://samples.auth0.com/passwordless/start', + emptySuccess, + ); + expect.assertions(1); + await auth.passwordlessWithEmail({ + email: 'info@auth0.com', + send: 'link', + }); + expect(fetchMock.lastCall()).toMatchSnapshot(); + }); + + it('should begin with optional parameters', async () => { + fetchMock.postOnce( + 'https://samples.auth0.com/passwordless/start', + emptySuccess, + ); + expect.assertions(1); + await auth.passwordlessWithEmail({ + email: 'info@auth0.com', + send: 'code', + authParams: { + scope: 'openid profile', + }, + }); + expect(fetchMock.lastCall()).toMatchSnapshot(); + }); + + it('should continue', async () => { + fetchMock.postOnce('https://samples.auth0.com/oauth/token', tokens); + expect.assertions(1); + await auth.loginWithEmail({ + email: 'info@auth0.com', + code: '123456', + }); + expect(fetchMock.lastCall()).toMatchSnapshot(); + }); + + it('should continue with optional parameters', async () => { + fetchMock.postOnce('https://samples.auth0.com/oauth/token', tokens); + expect.assertions(1); + await auth.loginWithEmail({ + email: 'info@auth0.com', + code: '123456', + audience: 'http://myapi.com', + scope: 'openid', + }); + expect(fetchMock.lastCall()).toMatchSnapshot(); + }); + }); + + describe('with SMS connection', () => { + it('should begin with code', async () => { + fetchMock.postOnce( + 'https://samples.auth0.com/passwordless/start', + emptySuccess, + ); + expect.assertions(1); + await auth.passwordlessWithSMS({ + phoneNumber: '+5491159991000', + send: 'code', + }); + expect(fetchMock.lastCall()).toMatchSnapshot(); + }); + + it('should begin with link', async () => { + fetchMock.postOnce( + 'https://samples.auth0.com/passwordless/start', + emptySuccess, + ); + expect.assertions(1); + await auth.passwordlessWithSMS({ + phoneNumber: '+5491159991000', + send: 'link', + }); + expect(fetchMock.lastCall()).toMatchSnapshot(); + }); + + it('should begin with optional parameters', async () => { + fetchMock.postOnce( + 'https://samples.auth0.com/passwordless/start', + emptySuccess, + ); + expect.assertions(1); + await auth.passwordlessWithSMS({ + phoneNumber: '+5491159991000', + send: 'code', + authParams: { + scope: 'openid profile', + }, + }); + expect(fetchMock.lastCall()).toMatchSnapshot(); + }); + + it('should continue', async () => { + fetchMock.postOnce('https://samples.auth0.com/oauth/token', tokens); + expect.assertions(1); + await auth.loginWithSMS({ + phoneNumber: '+5491159991000', + code: '123456', + }); + expect(fetchMock.lastCall()).toMatchSnapshot(); + }); + + it('should continue with optional parameters', async () => { + fetchMock.postOnce('https://samples.auth0.com/oauth/token', tokens); + expect.assertions(1); + await auth.loginWithSMS({ + phoneNumber: '+5491159991000', + code: '123456', + audience: 'http://myapi.com', + scope: 'openid', + }); + expect(fetchMock.lastCall()).toMatchSnapshot(); + }); + }); + }); + describe('password realm', () => { const parameters = { username: 'info@auth0.com', password: 'secret pass', realm: 'Username-Password-Authentication', audience: 'http://myapi.com', - scope: 'openid' + scope: 'openid', }; it('should send correct payload', async () => { fetchMock.postOnce('https://samples.auth0.com/oauth/token', tokens); @@ -185,7 +324,7 @@ describe('auth', () => { it('should handle unexpected error', async () => { fetchMock.postOnce( 'https://samples.auth0.com/oauth/token', - unexpectedError + unexpectedError, ); expect.assertions(1); await expect(auth.passwordRealm(parameters)).rejects.toMatchSnapshot(); @@ -195,7 +334,7 @@ describe('auth', () => { describe('refresh token', () => { const parameters = { refreshToken: 'a refresh token of a user', - scope: 'openid' + scope: 'openid', }; it('should send correct payload', async () => { fetchMock.postOnce('https://samples.auth0.com/oauth/token', tokens); @@ -219,7 +358,7 @@ describe('auth', () => { it('should handle unexpected error', async () => { fetchMock.postOnce( 'https://samples.auth0.com/oauth/token', - unexpectedError + unexpectedError, ); expect.assertions(1); await expect(auth.refreshToken(parameters)).rejects.toMatchSnapshot(); @@ -227,11 +366,11 @@ describe('auth', () => { }); describe('revoke token', () => { - const parameters = { refreshToken: 'a refresh token of a user' }; + const parameters = {refreshToken: 'a refresh token of a user'}; const success = { status: 200, body: null, - headers: { 'Content-Type': 'application/json' } + headers: {'Content-Type': 'application/json'}, }; it('should send correct payload', async () => { fetchMock.postOnce('https://samples.auth0.com/oauth/revoke', success); @@ -255,7 +394,7 @@ describe('auth', () => { it('should handle unexpected error', async () => { fetchMock.postOnce( 'https://samples.auth0.com/oauth/revoke', - unexpectedError + unexpectedError, ); expect.assertions(1); await expect(auth.revoke(parameters)).rejects.toMatchSnapshot(); @@ -263,7 +402,7 @@ describe('auth', () => { }); describe('user info', () => { - const parameters = { token: 'an access token of a user' }; + const parameters = {token: 'an access token of a user'}; const success = { status: 200, body: { @@ -276,9 +415,9 @@ describe('auth', () => { updated_at: 1497317424, picture: 'http://example.com/janedoe/me.jpg', 'http://mysite.com/claims/customer': 192837465, - 'http://mysite.com/claims/status': 'closed' + 'http://mysite.com/claims/status': 'closed', }, - headers: { 'Content-Type': 'application/json' } + headers: {'Content-Type': 'application/json'}, }; it('should send correct payload', async () => { fetchMock.getOnce('https://samples.auth0.com/userinfo', success); @@ -295,7 +434,7 @@ describe('auth', () => { it('should return successful non-oidc response', async () => { fetchMock.getOnce('https://samples.auth0.com/userinfo', { - sub: 'auth0|1029837475' + sub: 'auth0|1029837475', }); expect.assertions(1); await expect(auth.userInfo(parameters)).resolves.toMatchSnapshot(); @@ -317,17 +456,17 @@ describe('auth', () => { describe('change password', () => { const parameters = { email: 'info@auth0.com', - connection: 'Username-Password-Authentication' + connection: 'Username-Password-Authentication', }; const success = { status: 200, body: "We've just sent you an email to reset your password.", - headers: { 'Content-Type': 'text/html' } + headers: {'Content-Type': 'text/html'}, }; it('should send correct payload', async () => { fetchMock.postOnce( 'https://samples.auth0.com/dbconnections/change_password', - success + success, ); expect.assertions(1); await auth.resetPassword(parameters); @@ -337,7 +476,7 @@ describe('auth', () => { it('should return successful response', async () => { fetchMock.postOnce( 'https://samples.auth0.com/dbconnections/change_password', - success + success, ); expect.assertions(1); await expect(auth.resetPassword(parameters)).resolves.toMatchSnapshot(); @@ -346,7 +485,7 @@ describe('auth', () => { it('should handle oauth error', async () => { fetchMock.postOnce( 'https://samples.auth0.com/dbconnections/change_password', - oauthError + oauthError, ); expect.assertions(1); await expect(auth.resetPassword(parameters)).rejects.toMatchSnapshot(); @@ -355,7 +494,7 @@ describe('auth', () => { it('should handle unexpected error', async () => { fetchMock.postOnce( 'https://samples.auth0.com/dbconnections/change_password', - unexpectedError + unexpectedError, ); expect.assertions(1); await expect(auth.resetPassword(parameters)).rejects.toMatchSnapshot(); @@ -366,15 +505,15 @@ describe('auth', () => { const parameters = { email: 'info@auth0.com', password: 'secret', - connection: 'aconnection' + connection: 'aconnection', }; const success = { status: 200, body: { email: 'info@auth0.com', - email_verified: false + email_verified: false, }, - headers: { 'Content-Type': 'application/json' } + headers: {'Content-Type': 'application/json'}, }; const auth0Error = { status: 400, @@ -382,15 +521,15 @@ describe('auth', () => { code: 'user_exists', description: 'The user already exists.', name: 'BadRequestError', - statusCode: 400 + statusCode: 400, }, - headers: { 'Content-Type': 'application/json' } + headers: {'Content-Type': 'application/json'}, }; it('should send correct payload', async () => { fetchMock.postOnce( 'https://samples.auth0.com/dbconnections/signup', - success + success, ); expect.assertions(1); await auth.createUser(parameters); @@ -400,27 +539,27 @@ describe('auth', () => { it('should send correct payload with username', async () => { fetchMock.postOnce( 'https://samples.auth0.com/dbconnections/signup', - success + success, ); expect.assertions(1); - await auth.createUser({ ...parameters, usename: 'info' }); + await auth.createUser({...parameters, usename: 'info'}); expect(fetchMock.lastCall()).toMatchSnapshot(); }); it('should send correct payload with metadata', async () => { fetchMock.postOnce( 'https://samples.auth0.com/dbconnections/signup', - success + success, ); expect.assertions(1); - await auth.createUser({ ...parameters, metadata: { customerId: 12345 } }); + await auth.createUser({...parameters, metadata: {customerId: 12345}}); expect(fetchMock.lastCall()).toMatchSnapshot(); }); it('should return successful response', async () => { fetchMock.postOnce( 'https://samples.auth0.com/dbconnections/signup', - success + success, ); expect.assertions(1); await expect(auth.createUser(parameters)).resolves.toMatchSnapshot(); @@ -429,7 +568,7 @@ describe('auth', () => { it('should handle auth0 error', async () => { fetchMock.postOnce( 'https://samples.auth0.com/dbconnections/signup', - auth0Error + auth0Error, ); expect.assertions(1); await expect(auth.createUser(parameters)).rejects.toMatchSnapshot(); @@ -438,7 +577,7 @@ describe('auth', () => { it('should handle unexpected error', async () => { fetchMock.postOnce( 'https://samples.auth0.com/dbconnections/signup', - unexpectedError + unexpectedError, ); expect.assertions(1); await expect(auth.createUser(parameters)).rejects.toMatchSnapshot(); diff --git a/src/auth/index.js b/src/auth/index.js index c549c67b..d0c34db9 100644 --- a/src/auth/index.js +++ b/src/auth/index.js @@ -1,6 +1,6 @@ import Client from '../networking'; -import { apply } from '../utils/whitelist'; -import { toCamelCase } from '../utils/camel'; +import {apply} from '../utils/whitelist'; +import {toCamelCase} from '../utils/camel'; import AuthError from './authError'; import Auth0Error from './auth0Error'; @@ -21,7 +21,7 @@ function responseHandler(response, exceptions = {}) { export default class Auth { constructor(options = {}) { this.client = new Client(options); - const { clientId } = options; + const {clientId} = options; if (!clientId) { throw new Error('Missing clientId in parameters'); } @@ -45,18 +45,18 @@ export default class Auth { const query = apply( { parameters: { - redirectUri: { required: true, toName: 'redirect_uri' }, - responseType: { required: true, toName: 'response_type' }, - state: { required: true } + redirectUri: {required: true, toName: 'redirect_uri'}, + responseType: {required: true, toName: 'response_type'}, + state: {required: true}, }, - whitelist: false + whitelist: false, }, - parameters + parameters, ); return this.client.url( '/authorize', - { ...query, client_id: this.clientId }, - true + {...query, client_id: this.clientId}, + true, ); } @@ -76,14 +76,14 @@ export default class Auth { const query = apply( { parameters: { - federated: { required: false }, - clientId: { required: false, toName: 'client_id' }, - returnTo: { required: false } - } + federated: {required: false}, + clientId: {required: false, toName: 'client_id'}, + returnTo: {required: false}, + }, }, - parameters + parameters, ); - return this.client.url('/v2/logout', { ...query }, true); + return this.client.url('/v2/logout', {...query}, true); } /** @@ -102,18 +102,18 @@ export default class Auth { const payload = apply( { parameters: { - code: { required: true }, - verifier: { required: true, toName: 'code_verifier' }, - redirectUri: { required: true, toName: 'redirect_uri' } - } + code: {required: true}, + verifier: {required: true, toName: 'code_verifier'}, + redirectUri: {required: true, toName: 'redirect_uri'}, + }, }, - parameters + parameters, ); return this.client .post('/oauth/token', { ...payload, client_id: this.clientId, - grant_type: 'authorization_code' + grant_type: 'authorization_code', }) .then(responseHandler); } @@ -136,20 +136,20 @@ export default class Auth { const payload = apply( { parameters: { - username: { required: true }, - password: { required: true }, - realm: { required: true }, - audience: { required: false }, - scope: { required: false } - } + username: {required: true}, + password: {required: true}, + realm: {required: true}, + audience: {required: false}, + scope: {required: false}, + }, }, - parameters + parameters, ); return this.client .post('/oauth/token', { ...payload, client_id: this.clientId, - grant_type: 'http://auth0.com/oauth/grant-type/password-realm' + grant_type: 'http://auth0.com/oauth/grant-type/password-realm', }) .then(responseHandler); } @@ -169,17 +169,145 @@ export default class Auth { const payload = apply( { parameters: { - refreshToken: { required: true, toName: 'refresh_token' }, - scope: { required: false } - } + refreshToken: {required: true, toName: 'refresh_token'}, + scope: {required: false}, + }, }, - parameters + parameters, ); return this.client .post('/oauth/token', { ...payload, client_id: this.clientId, - grant_type: 'refresh_token' + grant_type: 'refresh_token', + }) + .then(responseHandler); + } + + /** + * Starts the Passworldess flow with an email connection + * + * @param {Object} parameters passwordless parameters + * @param {String} parameters.email the email to send the link/code to + * @param {String} parameters.send the passwordless strategy, either 'link' or 'code' + * @param {String} parameters.authParams optional parameters, used when strategy is 'linkĖ' + * @returns {Promise} + * + * @memberof Auth + */ + passwordlessWithEmail(parameters = {}) { + const payload = apply( + { + parameters: { + email: {required: true}, + send: {required: false}, + authParams: {required: false}, + }, + }, + parameters, + ); + return this.client + .post('/passwordless/start', { + ...payload, + connection: 'email', + client_id: this.clientId, + }) + .then(responseHandler); + } + + /** + * Starts the Passworldess flow with an SMS connection + * + * @param {Object} parameters passwordless parameters + * @param {String} parameters.phoneNumber the phone number to send the link/code to + * @returns {Promise} + * + * @memberof Auth + */ + passwordlessWithSMS(parameters = {}) { + const payload = apply( + { + parameters: { + phoneNumber: {required: true, toName: 'phone_number'}, + send: {required: false}, + authParams: {required: false}, + }, + }, + parameters, + ); + return this.client + .post('/passwordless/start', { + ...payload, + connection: 'sms', + client_id: this.clientId, + }) + .then(responseHandler); + } + + /** + * Finishes the Passworldess authentication with an email connection + * + * @param {Object} parameters passwordless parameters + * @param {String} parameters.email the email where the link/code was received + * @param {String} parameters.code the code numeric value (OTP) + * @param {String} parameters.audience optional API audience to request + * @param {String} parameters.scope optional scopes to request + * @returns {Promise} + * + * @memberof Auth + */ + loginWithEmail(parameters = {}) { + const payload = apply( + { + parameters: { + email: {required: true, toName: 'username'}, + code: {required: true, toName: 'otp'}, + audience: {required: false}, + scope: {required: false}, + }, + }, + parameters, + ); + return this.client + .post('/oauth/token', { + ...payload, + client_id: this.clientId, + realm: 'email', + grant_type: 'http://auth0.com/oauth/grant-type/passwordless/otp', + }) + .then(responseHandler); + } + + /** + * Finishes the Passworldess authentication with an SMS connection + * + * @param {Object} parameters passwordless parameters + * @param {String} parameters.phoneNumber the phone number where the code was received + * @param {String} parameters.code the code numeric value (OTP) + * @param {String} parameters.audience optional API audience to request + * @param {String} parameters.scope optional scopes to request + * @returns {Promise} + * + * @memberof Auth + */ + loginWithSMS(parameters = {}) { + const payload = apply( + { + parameters: { + phoneNumber: {required: true, toName: 'username'}, + code: {required: true, toName: 'otp'}, + audience: {required: false}, + scope: {required: false}, + }, + }, + parameters, + ); + return this.client + .post('/oauth/token', { + ...payload, + client_id: this.clientId, + realm: 'sms', + grant_type: 'http://auth0.com/oauth/grant-type/passwordless/otp', }) .then(responseHandler); } @@ -197,15 +325,15 @@ export default class Auth { const payload = apply( { parameters: { - refreshToken: { required: true, toName: 'token' } - } + refreshToken: {required: true, toName: 'token'}, + }, }, - parameters + parameters, ); return this.client .post('/oauth/revoke', { ...payload, - client_id: this.clientId + client_id: this.clientId, }) .then(response => { if (response.ok) { @@ -228,13 +356,13 @@ export default class Auth { const payload = apply( { parameters: { - token: { required: true } - } + token: {required: true}, + }, }, - parameters + parameters, ); - const { baseUrl, telemetry } = this.client; - const client = new Client({ baseUrl, telemetry, token: payload.token }); + const {baseUrl, telemetry} = this.client; + const client = new Client({baseUrl, telemetry, token: payload.token}); const claims = [ 'sub', 'name', @@ -255,12 +383,12 @@ export default class Auth { 'phone_number', 'phone_number_verified', 'address', - 'updated_at' + 'updated_at', ]; return client .get('/userinfo') .then(response => - responseHandler(response, { attributes: claims, whitelist: true }) + responseHandler(response, {attributes: claims, whitelist: true}), ); } @@ -278,16 +406,16 @@ export default class Auth { const payload = apply( { parameters: { - email: { required: true }, - connection: { required: true } - } + email: {required: true}, + connection: {required: true}, + }, }, - parameters + parameters, ); return this.client .post('/dbconnections/change_password', { ...payload, - client_id: this.clientId + client_id: this.clientId, }) .then(response => { if (response.ok) { @@ -314,20 +442,20 @@ export default class Auth { const payload = apply( { parameters: { - email: { required: true }, - password: { required: true }, - connection: { required: true }, - username: { required: false }, - metadata: { required: false, toName: 'user_metadata' } - } + email: {required: true}, + password: {required: true}, + connection: {required: true}, + username: {required: false}, + metadata: {required: false, toName: 'user_metadata'}, + }, }, - parameters + parameters, ); return this.client .post('/dbconnections/signup', { ...payload, - client_id: this.clientId + client_id: this.clientId, }) .then(response => { if (response.ok && response.json) {