2017-01-31 21:56:09 +00:00
////////////////////////////////////////////////////////////////////////////
//
// Copyright 2016 Realm Inc.
//
// 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.
//
////////////////////////////////////////////////////////////////////////////
2016-10-20 00:55:46 +00:00
'use strict' ;
2016-11-08 16:59:30 +00:00
const AuthError = require ( './errors' ) . AuthError ;
2017-08-29 13:23:22 +00:00
const permissionApis = require ( './permission-api' ) ;
2016-11-08 16:59:30 +00:00
2017-09-26 04:20:28 +00:00
const require _method = require ;
2016-10-20 00:55:46 +00:00
function node _require ( module ) {
2017-09-26 04:20:28 +00:00
return require _method ( module ) ;
2016-10-20 00:55:46 +00:00
}
2017-03-20 11:52:41 +00:00
function checkTypes ( args , types ) {
args = Array . prototype . slice . call ( args ) ;
for ( var i = 0 ; i < types . length ; ++ i ) {
2017-09-12 13:17:59 +00:00
if ( args . length > i && typeof args [ i ] !== types [ i ] ) {
2017-03-20 11:52:41 +00:00
throw new TypeError ( 'param ' + i + ' must be of type ' + types [ i ] ) ;
}
}
}
2018-03-15 23:08:48 +00:00
// Perform a HTTP request, enqueuing it if too many requests are already in
// progress to avoid hammering the server.
const performFetch = ( function ( ) {
const doFetch = typeof fetch === 'undefined' ? node _require ( 'node-fetch' ) : fetch ;
const queue = [ ] ;
let count = 0 ;
const maxCount = 5 ;
const next = ( ) => {
if ( count >= maxCount ) {
return ;
}
const req = queue . shift ( ) ;
if ( ! req ) {
return ;
}
const [ url , options , resolve , reject ] = req ;
++ count ;
// node doesn't support Promise.prototype.finally until 10
doFetch ( url , options )
. then ( response => {
-- count ;
next ( ) ;
resolve ( response ) ;
} )
. catch ( error => {
-- count ;
next ( ) ;
reject ( error ) ;
} ) ;
} ;
return ( url , options ) => {
return new Promise ( ( resolve , reject ) => {
queue . push ( [ url , options , resolve , reject ] ) ;
next ( ) ;
} ) ;
} ;
} ) ( ) ;
2016-10-20 00:55:46 +00:00
2017-01-31 21:56:09 +00:00
const url _parse = require ( 'url-parse' ) ;
2016-10-20 00:55:46 +00:00
2017-07-06 09:27:01 +00:00
const postHeaders = {
2016-10-20 00:55:46 +00:00
'content-type' : 'application/json;charset=utf-8' ,
'accept' : 'application/json'
} ;
2018-01-11 13:47:54 +00:00
function append _url ( server , path ) {
return server + ( server . charAt ( server . length - 1 ) != '/' ? '/' : '' ) + path ;
2016-11-08 16:59:30 +00:00
}
2017-02-03 15:40:13 +00:00
function scheduleAccessTokenRefresh ( user , localRealmPath , realmUrl , expirationDate ) {
const refreshBuffer = 10 * 1000 ;
const timeout = expirationDate - Date . now ( ) - refreshBuffer ;
setTimeout ( ( ) => refreshAccessToken ( user , localRealmPath , realmUrl ) , timeout ) ;
}
2017-06-27 18:32:34 +00:00
function print _error ( ) {
( console . error || console . log ) . apply ( console , arguments ) ;
}
2018-01-11 13:47:54 +00:00
function validateRefresh ( user , localRealmPath , response , json ) {
let session = user . _sessionForOnDiskPath ( localRealmPath ) ;
if ( ! session ) {
2018-03-21 17:18:37 +00:00
print _error ( ` Unhandled session token refresh error: could not look up session for user ${ user . identity } at path ${ localRealmPath } ` ) ;
2018-01-11 13:47:54 +00:00
return ;
}
const errorHandler = session . config . error ;
if ( response . status != 200 ) {
let error = new AuthError ( json ) ;
if ( errorHandler ) {
errorHandler ( session , error ) ;
} else {
2018-03-21 17:18:37 +00:00
print _error ( ` Unhandled session token refresh error for user ${ user . identity } at path ${ localRealmPath } ` , error ) ;
2018-01-11 13:47:54 +00:00
}
return ;
}
if ( session . state === 'invalid' ) {
return ;
}
return session ;
}
function refreshAdminToken ( user , localRealmPath , realmUrl ) {
const token = user . token ;
const server = user . server ;
// We don't need to actually refresh the token, but we need to let ROS know
// we're accessing the file and get the sync label for multiplexing
let parsedRealmUrl = url _parse ( realmUrl ) ;
const url = append _url ( user . server , 'realms/files/' + encodeURIComponent ( parsedRealmUrl . pathname ) ) ;
performFetch ( url , { method : 'GET' , timeout : 10000.0 , headers : { Authorization : user . token } } )
2018-01-13 09:02:08 +00:00
. then ( ( response ) => {
// There may not be a Realm Directory Service running on the server
// we're talking to. If we're talking directly to the sync service
// we'll get a 404, and if we're running inside ROS we'll get a 503 if
// the directory service hasn't started yet (perhaps because we got
// called due to the directory service itself opening some Realms).
//
// In both of these cases we can just pretend we got a valid response.
if ( response . status === 404 || response . status === 503 ) {
return { response : { status : 200 } , json : { path : parsedRealmUrl . pathname , syncLabel : '_direct' } } ;
}
else {
return response . json ( ) . then ( ( json ) => { return { response , json } ; } ) ;
}
} )
2018-01-11 13:47:54 +00:00
. then ( ( responseAndJson ) => {
const response = responseAndJson . response ;
const json = responseAndJson . json ;
const newUser = user . constructor . adminUser ( token , server ) ;
const session = validateRefresh ( newUser , localRealmPath , response , json ) ;
if ( session ) {
parsedRealmUrl . set ( 'pathname' , json . path ) ;
session . _refreshAccessToken ( user . token , parsedRealmUrl . href , json . syncLabel ) ;
}
2018-01-18 12:26:14 +00:00
} )
. catch ( ( e ) => {
print _error ( e ) ;
2018-04-12 16:20:25 +00:00
setTimeout ( ( ) => refreshAccessToken ( user , localRealmPath , realmUrl ) , 10 * 1000 ) ;
2018-01-11 13:47:54 +00:00
} ) ;
}
2017-02-03 15:40:13 +00:00
function refreshAccessToken ( user , localRealmPath , realmUrl ) {
2017-08-31 19:38:10 +00:00
if ( ! user . server ) {
throw new Error ( "Server for user must be specified" ) ;
}
2017-02-01 13:18:59 +00:00
let parsedRealmUrl = url _parse ( realmUrl ) ;
2018-01-11 13:47:54 +00:00
if ( user . isAdminToken ) {
return refreshAdminToken ( user , localRealmPath , realmUrl ) ;
}
const url = append _url ( user . server , 'auth' ) ;
2017-02-01 13:18:59 +00:00
const options = {
2017-01-31 13:07:29 +00:00
method : 'POST' ,
body : JSON . stringify ( {
data : user . token ,
2017-02-01 13:18:59 +00:00
path : parsedRealmUrl . pathname ,
2017-01-31 13:07:29 +00:00
provider : 'realm' ,
app _id : ''
} ) ,
2017-09-27 20:52:14 +00:00
headers : postHeaders ,
// FIXME: This timeout appears to be necessary in order for some requests to be sent at all.
// See https://github.com/realm/realm-js-private/issues/338 for details.
2017-12-21 13:14:07 +00:00
timeout : 10000.0
2017-01-31 13:07:29 +00:00
} ;
2017-02-01 13:18:59 +00:00
performFetch ( url , options )
2017-02-03 15:40:13 +00:00
. then ( ( response ) => response . json ( ) . then ( ( json ) => { return { response , json } ; } ) )
. then ( ( responseAndJson ) => {
const response = responseAndJson . response ;
const json = responseAndJson . json ;
2017-02-07 10:01:26 +00:00
// Look up a fresh instance of the user.
// We do this because in React Native Remote Debugging
// `Realm.clearTestState()` will have invalidated the user object
2018-01-11 15:00:31 +00:00
let newUser = user . constructor . _getExistingUser ( user . server , user . identity ) ;
2017-11-15 00:09:50 +00:00
if ( ! newUser ) {
return ;
}
2018-01-11 13:47:54 +00:00
const session = validateRefresh ( newUser , localRealmPath , response , json ) ;
if ( ! session ) {
2017-11-15 00:09:50 +00:00
return ;
2017-02-01 13:18:59 +00:00
}
2017-11-15 00:09:50 +00:00
const tokenData = json . access _token . token _data ;
parsedRealmUrl . set ( 'pathname' , tokenData . path ) ;
session . _refreshAccessToken ( json . access _token . token , parsedRealmUrl . href , tokenData . sync _label ) ;
2018-01-11 13:47:54 +00:00
const errorHandler = session . config . error ;
2017-11-15 00:09:50 +00:00
if ( errorHandler && errorHandler . _notifyOnAccessTokenRefreshed ) {
errorHandler ( session , errorHandler . _notifyOnAccessTokenRefreshed )
}
const tokenExpirationDate = new Date ( tokenData . expires * 1000 ) ;
scheduleAccessTokenRefresh ( newUser , localRealmPath , realmUrl , tokenExpirationDate ) ;
2017-08-31 17:39:48 +00:00
} )
2017-08-31 17:50:23 +00:00
. catch ( ( e ) => {
print _error ( e ) ;
// in case something lower in the HTTP stack breaks, try again in 10 seconds
setTimeout ( ( ) => refreshAccessToken ( user , localRealmPath , realmUrl ) , 10 * 1000 ) ;
} )
2017-01-31 13:07:29 +00:00
}
2017-08-24 18:01:12 +00:00
/ * *
* The base authentication method . It fires a JSON POST to the server parameter plus the auth url
* For example , if the server parameter is ` http://myapp.com ` , this url will post to ` http://myapp.com/auth `
2017-11-15 00:09:50 +00:00
* @ param { object } userConstructor
* @ param { string } server the http or https server url
2017-08-24 18:01:12 +00:00
* @ param { object } json the json to post to the auth endpoint
2017-11-15 00:09:50 +00:00
* @ param { Function } callback an optional callback with an error and user parameter
2017-08-24 18:01:12 +00:00
* @ returns { Promise } only returns a promise if the callback parameter was omitted
* /
2017-01-31 21:56:09 +00:00
function _authenticate ( userConstructor , server , json , callback ) {
json . app _id = '' ;
2018-01-11 13:47:54 +00:00
const url = append _url ( server , 'auth' ) ;
2017-01-31 21:56:09 +00:00
const options = {
method : 'POST' ,
body : JSON . stringify ( json ) ,
headers : postHeaders ,
open _timeout : 5000
} ;
2017-09-12 20:04:20 +00:00
2017-08-24 18:01:12 +00:00
const promise = performFetch ( url , options )
2017-07-06 09:27:01 +00:00
. then ( ( response ) => {
2017-12-07 09:36:24 +00:00
const contentType = response . headers . get ( 'Content-Type' ) ;
if ( contentType . indexOf ( 'application/json' ) === - 1 ) {
return response . text ( ) . then ( ( body ) => {
throw new AuthError ( {
title : ` Could not authenticate: Realm Object Server didn't respond with valid JSON ` ,
body ,
} ) ;
} ) ;
} else if ( ! response . ok ) {
2017-09-12 20:04:20 +00:00
return response . json ( ) . then ( ( body ) => Promise . reject ( new AuthError ( body ) ) ) ;
2017-01-31 21:56:09 +00:00
} else {
return response . json ( ) . then ( function ( body ) {
// TODO: validate JSON
const token = body . refresh _token . token ;
const identity = body . refresh _token . token _data . identity ;
2017-06-17 14:59:15 +00:00
const isAdmin = body . refresh _token . token _data . is _admin ;
2017-09-12 20:04:20 +00:00
return userConstructor . createUser ( server , identity , token , false , isAdmin ) ;
} ) ;
2017-01-31 21:56:09 +00:00
}
2017-08-24 18:01:12 +00:00
} ) ;
if ( callback ) {
2017-11-15 00:09:50 +00:00
promise . then ( user => {
callback ( null , user ) ;
2017-01-31 21:56:09 +00:00
} )
2017-08-24 18:01:12 +00:00
. catch ( err => {
2017-09-12 17:26:08 +00:00
callback ( err ) ;
2017-08-24 18:01:12 +00:00
} ) ;
} else {
return promise ;
}
2017-01-31 21:56:09 +00:00
}
2016-10-20 00:55:46 +00:00
2018-04-25 09:23:47 +00:00
function _updateAccount ( userConstructor , server , json ) {
const url = append _url ( server , 'auth/password/updateAccount' ) ;
const options = {
method : 'POST' ,
body : JSON . stringify ( json ) ,
headers : postHeaders ,
open _timeout : 5000
} ;
return performFetch ( url , options )
. then ( ( response ) => {
const contentType = response . headers . get ( 'Content-Type' ) ;
if ( contentType . indexOf ( 'application/json' ) === - 1 ) {
return response . text ( ) . then ( ( body ) => {
throw new AuthError ( {
title : ` Could not update user account: Realm Object Server didn't respond with valid JSON ` ,
body ,
} ) ;
} ) ;
}
if ( ! response . ok ) {
return response . json ( ) . then ( ( body ) => Promise . reject ( new AuthError ( body ) ) ) ;
}
} ) ;
}
2017-08-29 13:23:22 +00:00
const staticMethods = {
2017-01-31 21:56:09 +00:00
get current ( ) {
const allUsers = this . all ;
2017-01-31 13:07:29 +00:00
const keys = Object . keys ( allUsers ) ;
if ( keys . length === 0 ) {
return undefined ;
} else if ( keys . length > 1 ) {
throw new Error ( "Multiple users are logged in" ) ;
}
return allUsers [ keys [ 0 ] ] ;
2017-01-31 21:56:09 +00:00
} ,
2017-01-31 13:07:29 +00:00
2017-07-10 13:04:55 +00:00
adminUser ( token , server ) {
2017-08-31 19:38:10 +00:00
checkTypes ( arguments , [ 'string' , 'string' ] ) ;
2017-10-16 21:51:47 +00:00
return this . _adminUser ( server , token ) ;
2017-01-31 21:56:09 +00:00
} ,
register ( server , username , password , callback ) {
2017-03-20 11:52:41 +00:00
checkTypes ( arguments , [ 'string' , 'string' , 'string' , 'function' ] ) ;
2017-08-24 18:01:12 +00:00
const json = {
2017-07-06 09:27:01 +00:00
provider : 'password' ,
user _info : { password : password , register : true } ,
2017-01-31 21:56:09 +00:00
data : username
2017-08-24 18:01:12 +00:00
} ;
2017-11-15 00:09:50 +00:00
2017-09-12 17:38:43 +00:00
if ( callback ) {
const message = "register(..., callback) is now deprecated in favor of register(): Promise<User>. This function argument will be removed in future versions." ;
( console . warn || console . log ) . call ( console , message ) ;
}
2017-11-15 00:09:50 +00:00
2017-09-12 20:04:20 +00:00
return _authenticate ( this , server , json , callback ) ;
2017-01-31 21:56:09 +00:00
} ,
login ( server , username , password , callback ) {
2017-03-20 11:52:41 +00:00
checkTypes ( arguments , [ 'string' , 'string' , 'string' , 'function' ] ) ;
2017-08-24 18:01:12 +00:00
const json = {
2017-07-06 09:27:01 +00:00
provider : 'password' ,
2018-01-08 09:32:05 +00:00
user _info : { password : password , register : false } ,
2017-01-31 21:56:09 +00:00
data : username
2017-08-24 18:01:12 +00:00
} ;
2017-11-15 00:09:50 +00:00
2017-09-12 17:38:43 +00:00
if ( callback ) {
const message = "login(..., callback) is now deprecated in favor of login(): Promise<User>. This function argument will be removed in future versions." ;
( console . warn || console . log ) . call ( console , message ) ;
}
2017-09-12 20:04:20 +00:00
return _authenticate ( this , server , json , callback ) ;
2017-01-31 21:56:09 +00:00
} ,
2017-03-17 13:13:03 +00:00
registerWithProvider ( server , options , callback ) {
2017-11-15 00:09:50 +00:00
// Compatibility with previous signature:
2017-03-17 13:13:03 +00:00
// registerWithProvider(server, provider, providerToken, callback)
if ( arguments . length === 4 ) {
2017-03-20 11:52:41 +00:00
checkTypes ( arguments , [ 'string' , 'string' , 'string' , 'function' ] ) ;
2017-03-17 13:13:03 +00:00
options = {
provider : arguments [ 1 ] ,
providerToken : arguments [ 2 ]
} ;
2017-07-06 09:27:01 +00:00
callback = arguments [ 3 ] ;
2017-03-20 11:52:41 +00:00
} else {
checkTypes ( arguments , [ 'string' , 'object' , 'function' ] ) ;
2017-03-17 13:13:03 +00:00
}
2017-08-24 18:01:12 +00:00
let json = {
2017-03-17 13:13:03 +00:00
provider : options . provider ,
data : options . providerToken ,
} ;
if ( options . userInfo ) {
2017-08-24 18:01:12 +00:00
json . user _info = options . userInfo ;
2017-03-17 13:13:03 +00:00
}
2017-09-12 17:38:43 +00:00
if ( callback ) {
const message = "registerWithProvider(..., callback) is now deprecated in favor of registerWithProvider(): Promise<User>. This function argument will be removed in future versions." ;
( console . warn || console . log ) . call ( console , message ) ;
}
2017-11-15 00:09:50 +00:00
2017-09-12 20:04:20 +00:00
return _authenticate ( this , server , json , callback ) ;
2017-01-31 21:56:09 +00:00
} ,
2018-01-05 08:38:53 +00:00
authenticate ( server , provider , options ) {
checkTypes ( arguments , [ 'string' , 'string' , 'object' ] )
var json = { }
switch ( provider . toLowerCase ( ) ) {
case 'jwt' :
json . provider = 'jwt'
json . token = options . token ;
break
case 'password' :
json . provider = 'password'
json . user _info = { password : options . password }
json . data = options . username
break
default :
Object . assign ( json , options )
json . provider = provider
}
return _authenticate ( this , server , json )
} ,
2018-04-25 09:23:47 +00:00
requestPasswordReset ( server , email ) {
checkTypes ( arguments , [ 'string' , 'string' ] ) ;
const json = {
provider _id : email ,
data : { action : 'reset_password' }
} ;
return _updateAccount ( this , server , json ) ;
} ,
completePasswordReset ( server , reset _token , new _password ) {
checkTypes ( arguments , [ 'string' , 'string' ] ) ;
const json = {
data : {
action : 'complete_reset' ,
token : reset _token ,
new _password : new _password
}
} ;
return _updateAccount ( this , server , json ) ;
} ,
requestEmailConfirmation ( server , email ) {
checkTypes ( arguments , [ 'string' , 'string' ] ) ;
const json = {
provider _id : email ,
data : { action : 'request_email_confirmation' }
} ;
return _updateAccount ( this , server , json ) ;
} ,
confirmEmail ( server , confirmation _token ) {
checkTypes ( arguments , [ 'string' , 'string' ] ) ;
const json = {
data : {
action : 'confirm_email' ,
token : confirmation _token
}
} ;
return _updateAccount ( this , server , json ) ;
} ,
2018-01-05 08:38:53 +00:00
_refreshAccessToken : refreshAccessToken ,
} ;
2017-08-29 13:23:22 +00:00
const instanceMethods = {
2018-03-09 14:51:45 +00:00
logout ( ) {
this . _logout ( ) ;
const url = url _parse ( this . server ) ;
url . set ( 'pathname' , '/auth/revoke' ) ;
const headers = {
Authorization : this . token
} ;
2018-04-17 13:01:02 +00:00
const body = JSON . stringify ( {
2018-03-09 14:51:45 +00:00
token : this . token
2018-04-17 13:01:02 +00:00
} ) ;
2018-03-09 14:51:45 +00:00
const options = {
method : 'POST' ,
headers ,
body : body ,
open _timeout : 5000
} ;
performFetch ( url . href , options )
. then ( ( ) => console . log ( 'User is logged out' ) )
. catch ( ( e ) => print _error ( e ) ) ;
} ,
2017-01-31 21:56:09 +00:00
openManagementRealm ( ) {
let url = url _parse ( this . server ) ;
if ( url . protocol === 'http:' ) {
url . set ( 'protocol' , 'realm:' ) ;
} else if ( url . protocol === 'https:' ) {
url . set ( 'protocol' , 'realms:' ) ;
} else {
throw new Error ( ` Unexpected user auth url: ${ this . server } ` ) ;
}
2017-01-31 13:07:29 +00:00
2017-01-31 21:56:09 +00:00
url . set ( 'pathname' , '/~/__management' ) ;
2017-02-08 12:36:43 +00:00
return new this . constructor . _realmConstructor ( {
2017-01-31 21:56:09 +00:00
schema : require ( './management-schema' ) ,
sync : {
user : this ,
url : url . href
}
} ) ;
2017-07-03 13:55:18 +00:00
} ,
retrieveAccount ( provider , provider _id ) {
checkTypes ( arguments , [ 'string' , 'string' ] ) ;
2017-07-07 13:38:13 +00:00
const url = url _parse ( this . server ) ;
2017-09-22 17:10:04 +00:00
url . set ( 'pathname' , ` /auth/users/ ${ provider } / ${ provider _id } ` ) ;
2017-07-07 13:38:13 +00:00
const headers = {
2017-07-06 09:27:01 +00:00
Authorization : this . token
2017-07-03 13:55:18 +00:00
} ;
const options = {
method : 'GET' ,
2017-07-07 13:38:13 +00:00
headers ,
2017-07-03 13:55:18 +00:00
open _timeout : 5000
} ;
return performFetch ( url . href , options )
2017-07-06 09:27:01 +00:00
. then ( ( response ) => {
2017-07-03 13:55:18 +00:00
if ( response . status !== 200 ) {
2017-07-06 09:27:01 +00:00
return response . json ( )
. then ( body => {
throw new AuthError ( body ) ;
} ) ;
2017-07-03 13:55:18 +00:00
} else {
return response . json ( ) ;
}
} ) ;
} ,
2017-08-29 13:23:22 +00:00
} ;
// Append the permission apis
Object . assign ( instanceMethods , permissionApis ) ;
module . exports = {
static : staticMethods ,
instance : instanceMethods
} ;