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 ] ) ;
}
}
}
2017-07-10 14:53:00 +00:00
/* global fetch */
2017-01-31 13:07:29 +00:00
const performFetch = typeof fetch === 'undefined' ? node _require ( 'node-fetch' ) : fetch ;
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'
} ;
2016-11-08 16:59:30 +00:00
function auth _url ( server ) {
2017-07-06 09:27:01 +00:00
if ( server . charAt ( server . length - 1 ) != '/' ) {
2016-11-08 16:59:30 +00:00
return server + '/auth' ;
}
return server + 'auth' ;
}
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 ) ;
}
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 ) ;
const url = auth _url ( user . server ) ;
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.
timeout : 1000.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
let newUser = user . constructor . all [ user . identity ] ;
if ( newUser ) {
let session = newUser . _sessionForOnDiskPath ( localRealmPath ) ;
if ( session ) {
2017-03-24 11:49:28 +00:00
const errorHandler = session . config . error ;
2017-02-07 10:01:26 +00:00
if ( response . status != 200 ) {
let error = new AuthError ( json ) ;
if ( errorHandler ) {
errorHandler ( session , error ) ;
} else {
2017-06-27 18:32:34 +00:00
print _error ( 'Unhandled session token refresh error' , error ) ;
2017-02-07 10:01:26 +00:00
}
} else if ( session . state !== 'invalid' ) {
2017-02-03 15:40:13 +00:00
parsedRealmUrl . set ( 'pathname' , json . access _token . token _data . path ) ;
session . _refreshAccessToken ( json . access _token . token , parsedRealmUrl . href ) ;
2017-03-24 11:49:28 +00:00
if ( errorHandler && errorHandler . _notifyOnAccessTokenRefreshed ) {
errorHandler ( session , errorHandler . _notifyOnAccessTokenRefreshed )
}
2017-02-03 15:40:13 +00:00
const tokenExpirationDate = new Date ( json . access _token . token _data . expires * 1000 ) ;
scheduleAccessTokenRefresh ( newUser , localRealmPath , realmUrl , tokenExpirationDate ) ;
2017-02-01 22:44:56 +00:00
}
2017-06-27 18:32:34 +00:00
} else {
print _error ( ` Unhandled session token refresh error: could not look up session at path ${ localRealmPath } ` ) ;
2017-02-03 15:40:13 +00:00
}
2017-02-01 13:18:59 +00:00
}
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-10-13 12:39:28 +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-10-13 12:39:28 +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 = '' ;
const url = auth _url ( server ) ;
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-01-31 21:56:09 +00:00
if ( response . status !== 200 ) {
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-10-13 12:39:28 +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
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-13 16:12:30 +00:00
const user = this . _adminUser ( server , token ) ;
// FIXME: find a better way to detect that token or server is invalid
// check if object is empty
var isEmpty = true ;
for ( var prop in user ) {
if ( user . hasOwnProperty ( prop ) ) {
isEmpty = false ;
}
}
if ( isEmpty ) {
2017-10-13 16:18:05 +00:00
throw new Error ( 'Invalid adminToken or server.' ) ;
2017-10-13 16:12:30 +00:00
}
return user ;
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-10-13 12:39:28 +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-10-13 12:39:28 +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' ,
user _info : { password : password } ,
2017-01-31 21:56:09 +00:00
data : username
2017-08-24 18:01:12 +00:00
} ;
2017-10-13 12:39:28 +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-10-13 12:39:28 +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-10-13 12:39:28 +00:00
2017-09-12 20:04:20 +00:00
return _authenticate ( this , server , json , callback ) ;
2017-01-31 21:56:09 +00:00
} ,
2017-02-03 15:40:13 +00:00
_refreshAccessToken : refreshAccessToken
2017-08-29 13:23:22 +00:00
} ;
const instanceMethods = {
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
} ;