0.0.8 • Published 5 years ago
reactxp-msaadalauth v0.0.8
ReactXP-MsaAdalAuth
An abstraction around ADAL plus a manual MSA implementation for unified authentication support for ReactXP apps.
Installation
npm install --save reactxp-msaadalauth
To use with react native platforms, install the appropriate ADAL library for your platform(s):
- https://github.com/AzureAD/azure-activedirectory-library-for-objc
- https://github.com/AzureAD/azure-activedirectory-library-for-android
Sample Usage
Partial usage example stolen from a private app.
class AuthService implements Service {
private _auth!: MsaAdalAuth;
startup(): SyncTasks.Promise<void> {
let adalRedirectUri: string;
if (AppConfig.getPlatformType() === 'web') {
adalRedirectUri = window.location.origin;
} else if (AppConfig.getPlatformType() === 'windows') {
// Windows uses an auto-generated redirect uri from your app's SID, so this is ignored.
adalRedirectUri = '';
} else {
// Brokered auth
adalRedirectUri = 'x-msauth-xyz://com.blah.myapp';
}
const adalConfig: MsaAdalAppConfig = {
clientId: (AppConfig.getPlatformType() === 'web') ?
AdalWebClientId :
AdalNativeClientId,
redirectUri: adalRedirectUri,
};
const msaConfig: MsaAdalAppConfig = {
clientId: (AppConfig.getPlatformType() === 'web') ?
MsaWebClientId :
MsaNativeClientId,
clientSecret: (AppConfig.getPlatformType() === 'web') ?
undefined :
MsaNativeClientSecret,
redirectUri: (AppConfig.getPlatformType() === 'web') ?
window.location.origin :
'',
};
this._auth = new MsaAdalAuth(msaConfig, adalConfig, this._possibleLoginCallback);
return this._auth.processStartupLogins();
}
loginNewUser(): SyncTasks.Promise<void> {
const msaScopes = [GraphUserUrlScope];
const adalResourceId = OutlookResourceId;
// If we have any MSA-logged-in users, start with an AAD login. On web, MSA will force you to log back out
// again before letting you do an AAD login. On native, it auto redirects over to AAD.
const startWithAdal = _.some(this._users, u => u.enabled && u.isMsa);
return this._auth.loginNewUser(msaScopes, adalResourceId, !startWithAdal);
}
private _possibleLoginCallback = (result: UserLoginResult) => {
// Partial logins are from MSA, since AAD comes back with full info for us.
if (result.partial) {
// See if there's an existing user already for this.
const existingUser = _.find(this._users, user => user.anchorMailbox === result.partial!!!.anchorMailbox);
if (existingUser) {
// See if there's an existing token on the user that matches scopes of the incoming token.
let newTokenSet = _.clone(existingUser.accessTokens);
const existingTokenIndex = _.findIndex(existingUser.accessTokens, token =>
_.isEqual(token.scopes, result.partial!!!.accessToken.scopes));
if (existingTokenIndex !== -1) {
newTokenSet.splice(existingTokenIndex, 1, result.partial.accessToken);
} else {
newTokenSet.push(result.partial.accessToken);
}
const updatedUser = _.extend({}, existingUser, { enabled: true, accessTokens: newTokenSet });
this._users[updatedUser.userKey] = updatedUser;
this._trackNewUser(updatedUser, true);
return this._finishLoginOrUpdate();
}
// We have a partial login from MSA, but we need user info to finish it, so query for user info then
// wrap back around to the login result.
let promise: SyncTasks.Promise<void>;
if (AppConfig.getPlatformType() === 'web') {
promise = GraphClient.getMyInfo(result.partial.accessToken.token).then(info => {
const finalResult: UserLoginResult = {
full: {
userIdentifier: info.id,
displayName: info.displayName,
email: info.userPrincipalName,
anchorMailbox: result.partial!!!.anchorMailbox!!!,
refreshToken: result.partial!!!.refreshToken,
accessToken: result.partial!!!.accessToken!!!,
isMsa: true,
}
};
return this._possibleLoginCallback(finalResult);
});
} else {
promise = ProfileClient.getMyInfo(result.partial.accessToken.token, result.partial.anchorMailbox).then(info => {
const finalResult: UserLoginResult = {
full: {
userIdentifier: result.partial!!!.anchorMailbox,
displayName: info.names[0].displayNameDefault,
email: info.accounts[0].userPrincipalName,
anchorMailbox: result.partial!!!.anchorMailbox,
refreshToken: result.partial!!!.refreshToken,
accessToken: result.partial!!!.accessToken,
isMsa: true,
}
};
return this._possibleLoginCallback(finalResult);
});
}
return promise.catch(() => {
// TODO: Do we get a username hint with the login so we can at least try again?
return this.loginNewUser();
});
}
if (result.full) {
// Full and fresh login to store.
const user = result.full;
Instrumentation.log(LogTraceAreas.Auth, 'Logged in user: ' + JSON.stringify(user));
const userKey = user.userIdentifier as UserKey;
ServiceStateStore.internal_userLoggedIn(userKey);
const lastUser = CurrentUsersStore.getUser(userKey);
const newUser: User = {
enabled: true,
userKey,
accessTokens: _.compact([user.accessToken]),
refreshToken: user.refreshToken,
anchorMailbox: user.anchorMailbox,
fullName: user.displayName,
email: user.email,
isMsa: user.isMsa,
};
this._trackNewUser(newUser, true);
return this._finishLoginOrUpdate();
}
return SyncTasks.Resolved();
}
getAuthToken(user: User, resourceHost: ResourceHost) {
let applicableScope: string;
let scopesToFetch: string[];
if (user.isMsa) {
applicableScope = GraphUserUrlScope;
scopesToFetch = [applicableScope];
} else {
applicableScope = OutlookResourceId;
scopesToFetch = [OutlookResourceId];
}
const usableToken = _.find(user.accessTokens, token => _.includes(token.scopes, applicableScope));
if (usableToken) {
return SyncTasks.Resolved(usableToken.token);
}
if (user.isMsa) {
return this._auth.getMsaAuthToken(user.userKey, user.email, scopesToFetch, user.refreshToken);
} else {
return this._auth.getAdalAuthToken(user.userKey, user.email, applicableScope, user.refreshToken);
}
}
}