Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improved handling of AAD and AAD B2C signin/signup process. #621

Merged
merged 3 commits into from
May 19, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export class DocumentDetailsHandlers implements IWidgetHandler {
displayName: widgetDisplayName,
category: widgetCategory,
iconClass: "paperbits-puzzle-10",
requires: ["html"],
createModel: async () => {
const model = new DocumentDetailsModel();
model.fileName = defaultFileName;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ export class DocumentDetailsViewModelBinder implements ViewModelBinder<DocumentD
readonly: bindingContext?.readonly,
model: model,
editor: widgetEditorSelector,
draggable: true,
applyChanges: async () => {
await this.updateViewModel(model, viewModel);
this.eventManager.dispatchEvent("onContentUpdate");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,15 +30,14 @@ export class DocumentDetailsRuntime {

@OnMounted()
public async initialize(): Promise<void> {
//const fileName = this.fileName();
const fileName = this.routeHelper.getFileName(); // This is how you get filename from URL hash part.
const fileName = this.routeHelper.getHashParameter("fileName");
const api = this.routeHelper.getApiName();

const request: HttpRequest = {
url: `${documentApiUrl}/${fileName}`,
method: "GET"
};

const response = await this.httpClient.send<string>(request);
const sessionDescription = response.toText();

Expand Down
13 changes: 12 additions & 1 deletion src/components/users/signin-social/ko/runtime/signin-aad-b2c.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { MapiError } from "./../../../../../services/mapiError";
import * as ko from "knockout";
import template from "./signin-aad-b2c.html";
import { Component, RuntimeComponent, OnMounted, Param } from "@paperbits/common/ko/decorators";
Expand Down Expand Up @@ -59,10 +60,20 @@ export class SignInAadB2C {
await this.aadService.signInWithAadB2C(this.clientId(), this.authority(), this.instance(), this.signInPolicy());
}
catch (error) {
let errorDetails;

if (error.code === "ValidationError") {
errorDetails = error.details?.map(detail => detail.message);
}
else {
errorDetails = [error.message];
}

const validationReport: ValidationReport = {
source: "socialAcc",
errors: [error.message]
errors: errorDetails
};

this.eventManager.dispatchEvent("onValidationErrors", validationReport);
}
}
Expand Down
12 changes: 11 additions & 1 deletion src/components/users/signin-social/ko/runtime/signin-aad.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,10 +46,20 @@ export class SignInAad {
await this.aadService.signInWithAadAdal(this.clientId(), this.signinTenant());
}
catch (error) {
let errorDetails;

if (error.code === "ValidationError") {
errorDetails = error.details?.map(detail => detail.message);
}
else {
errorDetails = [error.message];
}

const validationReport: ValidationReport = {
source: "socialAcc",
errors: [error.message]
errors: errorDetails
};

this.eventManager.dispatchEvent("onValidationErrors", validationReport);
}
}
Expand Down
66 changes: 6 additions & 60 deletions src/components/users/signup-social/ko/runtime/signup-social.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,13 @@ import * as ko from "knockout";
import * as validation from "knockout.validation";
import * as Constants from "../../../../../constants";
import template from "./signup-social.html";
import { Component, RuntimeComponent, OnMounted, Param } from "@paperbits/common/ko/decorators";
import { Component, RuntimeComponent, OnMounted } from "@paperbits/common/ko/decorators";
import { EventManager } from "@paperbits/common/events";
import { Router } from "@paperbits/common/routing";
import { HttpClient } from "@paperbits/common/http";
import { ISettingsProvider } from "@paperbits/common/configuration";
import { ValidationReport } from "../../../../../contracts/validationReport";
import { UserPropertiesContract } from "../../../../../contracts/user";
import { Utils } from "../../../../../utils";
import { RouteHelper } from "../../../../../routing/routeHelper";
import { IAuthenticator } from "./../../../../../authentication/IAuthenticator";
import { MapiError } from "../../../../../services/mapiError";
import { UsersService } from "../../../../../services";


@RuntimeComponent({
Expand All @@ -30,11 +26,9 @@ export class SignupSocial {

constructor(
private readonly eventManager: EventManager,
private readonly httpClient: HttpClient,
private readonly settingsProvider: ISettingsProvider,
private readonly router: Router,
private readonly routeHelper: RouteHelper,
private readonly authenticator: IAuthenticator
private readonly usersService: UsersService
) {
this.email = ko.observable("");
this.firstName = ko.observable("");
Expand Down Expand Up @@ -72,55 +66,6 @@ export class SignupSocial {
this.email(jwtToken.email);
}

public async createUserWithOAuth(provider: string, idToken: string): Promise<void> {
const managementApiUrl = await this.settingsProvider.getSetting<string>("managementApiUrl");
const managementApiVersion = await this.settingsProvider.getSetting<string>("managementApiVersion");
const jwtToken = Utils.parseJwt(idToken);

const user: UserPropertiesContract = {
firstName: this.firstName(),
lastName: this.lastName(),
email: this.email(),
identities: [{
id: jwtToken.oid,
provider: provider
}]
};

const response = await this.httpClient.send({
url: `${managementApiUrl}/users?api-version=${managementApiVersion}`,
method: "POST",
headers: [
{ name: "Content-Type", value: "application/json" },
{ name: "Authorization", value: `${provider} id_token="${idToken}"` }
],
body: JSON.stringify(user)
});

if (!(response.statusCode >= 200 && response.statusCode <= 299)) {
throw MapiError.fromResponse(response);
}

const sasTokenHeader = response.headers.find(x => x.name.toLowerCase() === "ocp-apim-sas-token");

if (!sasTokenHeader) { // User not registered with APIM.
throw new Error("Unable to authenticate.");
return;
}

const regex = /token=\"(.*==)\"/gm;
const matches = regex.exec(sasTokenHeader.value);

if (!matches || matches.length < 1) {
throw new Error("Authentication failed. Unable to parse access token.");
}

const sasToken = matches[1];
await this.authenticator.setAccessToken(`SharedAccessSignature ${sasToken}`);

this.router.navigateTo(Constants.pageUrlHome);
}

/**
* Sends user signup request to Management API.
*/
Expand Down Expand Up @@ -159,9 +104,10 @@ export class SignupSocial {
errors: []
};

await this.createUserWithOAuth(provider, idToken);

this.eventManager.dispatchEvent("onValidationErrors", validationReport);

await this.usersService.createUserWithOAuth(provider, idToken, this.firstName(), this.lastName(), this.email());
await this.router.navigateTo(Constants.pageUrlHome);
}
catch (error) {
let errorMessages: string[];
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<div class="text-hide alert alert-danger" role="region" aria-live="assertive" aria-atomic="true" data-bind="css: { 'text-hide': !hasErrors() }">
<div class="alert alert-danger text-hide" role="region" aria-live="assertive" aria-atomic="true" data-bind="css: { 'text-hide': !hasErrors() }">
<!-- ko foreach: { data: errorMsgs, as: 'msg' } -->
<div data-bind="text: msg"></div>
<!-- /ko -->
Expand Down
5 changes: 5 additions & 0 deletions src/contracts/jwtToken.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,4 +85,9 @@ export interface JwtToken {
* Email address.
*/
email: string;

/**
* Array of email addresses, e.g. AAD B2C.
*/
emails: string;
}
2 changes: 1 addition & 1 deletion src/routing/routeHelper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { Router } from "@paperbits/common/routing";
export class RouteHelper {
constructor(private readonly router: Router) { }

private getHashParameter(name: string): string {
public getHashParameter(name: string): string {
const route = this.router.getCurrentRoute();
const params = new URLSearchParams(`?${route.hash}`);

Expand Down
44 changes: 24 additions & 20 deletions src/services/aadService.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import * as Msal from "msal";
import * as AuthenticationContext from "adal-vanilla";
import * as Constants from "../constants";
import { Utils } from "../utils";
import { IAuthenticator } from "../authentication";
import { Router } from "@paperbits/common/routing";
import { HttpClient } from "@paperbits/common/http";
import { ISettingsProvider } from "@paperbits/common/configuration";
import { RouteHelper } from "../routing/routeHelper";
import { UsersService } from "./usersService";


/**
Expand All @@ -17,7 +19,8 @@ export class AadService {
private readonly httpClient: HttpClient,
private readonly settingsProvider: ISettingsProvider,
private readonly router: Router,
private readonly routeHelper: RouteHelper
private readonly routeHelper: RouteHelper,
private readonly usersService: UsersService
) { }

/**
Expand All @@ -39,8 +42,20 @@ export class AadService {
const sasTokenHeader = response.headers.find(x => x.name.toLowerCase() === "ocp-apim-sas-token");

if (!sasTokenHeader) { // User not registered with APIM.
const signupUrl = this.routeHelper.getIdTokenReferenceUrl(provider, idToken);
await this.router.navigateTo(signupUrl);
const jwtToken = Utils.parseJwt(idToken);
const firstName = jwtToken.given_name;
const lastName = jwtToken.family_name;
const email = jwtToken.email || jwtToken.emails?.[0];

if (firstName && lastName && email) {
await this.usersService.createUserWithOAuth(provider, idToken, firstName, lastName, email);
await this.router.navigateTo(Constants.pageUrlHome);
}
else {
const signupUrl = this.routeHelper.getIdTokenReferenceUrl(provider, idToken);
await this.router.navigateTo(signupUrl);
}

return;
}

Expand Down Expand Up @@ -77,15 +92,10 @@ export class AadService {
scopes: ["openid", "email", "profile"]
};

try {
const response = await msalInstance.loginPopup(loginRequest);
const response = await msalInstance.loginPopup(loginRequest);

if (response.idToken && response.idToken.rawIdToken) {
await this.exchangeIdToken(response.idToken.rawIdToken, Constants.IdentityProviders.aad);
}
}
catch (error) {
throw new Error(`Unable to obtain id_token with client ID: ${aadClientId}. Error: ${error.message}`);
if (response.idToken && response.idToken.rawIdToken) {
await this.exchangeIdToken(response.idToken.rawIdToken, Constants.IdentityProviders.aad);
}
}

Expand Down Expand Up @@ -139,7 +149,6 @@ export class AadService {
throw new Error(`Authority not specified.`);
}


const auth = `https://${authority}/tfp/${instance}/${signInPolicy}`;

const msalConfig = {
Expand All @@ -156,15 +165,10 @@ export class AadService {
scopes: ["openid", "email", "profile"]
};

try {
const response = await msalInstance.loginPopup(loginRequest);
const response = await msalInstance.loginPopup(loginRequest);

if (response.idToken && response.idToken.rawIdToken) {
await this.exchangeIdToken(response.idToken.rawIdToken, Constants.IdentityProviders.aadB2C);
}
}
catch (error) {
throw new Error(`Unable to obtain id_token with client ID: ${clientId}. Error: ${error.message}`);
if (response.idToken && response.idToken.rawIdToken) {
await this.exchangeIdToken(response.idToken.rawIdToken, Constants.IdentityProviders.aadB2C);
}
}

Expand Down
62 changes: 57 additions & 5 deletions src/services/usersService.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
import * as Constants from "./../constants";
import { Router } from "@paperbits/common/routing";
import { HttpHeader, HttpClient } from "@paperbits/common/http";
import { ISettingsProvider } from "@paperbits/common/configuration";
import { IAuthenticator } from "../authentication";
import { MapiClient } from "./mapiClient";
import { Router } from "@paperbits/common/routing";
import { HttpHeader } from "@paperbits/common/http";
import { User } from "../models/user";
import { Utils } from "../utils";
import { Identity } from "../contracts/identity";
import { UserContract, } from "../contracts/user";
import { UserContract, UserPropertiesContract, } from "../contracts/user";
import { MapiSignupRequest } from "../contracts/signupRequest";
import { MapiError } from "./mapiError";


/**
* A service for management operations with users.
Expand All @@ -17,6 +20,8 @@ export class UsersService {
private readonly mapiClient: MapiClient,
private readonly router: Router,
private readonly authenticator: IAuthenticator,
private readonly httpClient: HttpClient,
private readonly settingsProvider: ISettingsProvider
) { }

/**
Expand Down Expand Up @@ -156,8 +161,6 @@ export class UsersService {
}
}



/**
* Deletes specified user.
* @param userId {string} Unique user identifier.
Expand Down Expand Up @@ -229,4 +232,53 @@ export class UsersService {
const payload = { password: newPassword };
await this.mapiClient.patch(userId, headers, payload);
}

public async createUserWithOAuth(provider: string, idToken: string, firstName: string, lastName: string, email: string): Promise<void> {
const managementApiUrl = await this.settingsProvider.getSetting<string>("managementApiUrl");
const managementApiVersion = await this.settingsProvider.getSetting<string>("managementApiVersion");
const jwtToken = Utils.parseJwt(idToken);

const user: UserPropertiesContract = {
firstName: firstName,
lastName: lastName,
email: email,
identities: [{
id: jwtToken.oid,
provider: provider
}]
};

const response = await this.httpClient.send({
url: `${managementApiUrl}/users?api-version=${managementApiVersion}`,
method: "POST",
headers: [
{ name: "Content-Type", value: "application/json" },
{ name: "Authorization", value: `${provider} id_token="${idToken}"` }
],
body: JSON.stringify(user)
});

if (!(response.statusCode >= 200 && response.statusCode <= 299)) {
throw MapiError.fromResponse(response);
}

const sasTokenHeader = response.headers.find(x => x.name.toLowerCase() === "ocp-apim-sas-token");

if (!sasTokenHeader) { // User not registered with APIM.
throw new Error("Unable to authenticate.");
return;
}

const regex = /token=\"(.*==)\"/gm;
const matches = regex.exec(sasTokenHeader.value);

if (!matches || matches.length < 1) {
throw new Error("Authentication failed. Unable to parse access token.");
}

const sasToken = matches[1];
await this.authenticator.setAccessToken(`SharedAccessSignature ${sasToken}`);

this.router.navigateTo(Constants.pageUrlHome);
}
}