Skip to content

Commit

Permalink
Added support for "resource owner password" grant flow. (#1219)
Browse files Browse the repository at this point in the history
  • Loading branch information
azaslonov authored Apr 22, 2021
1 parent aba9c3e commit 40839a0
Show file tree
Hide file tree
Showing 7 changed files with 136 additions and 19 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,41 @@ <h3>Authorization</h3>
</div>
</div>
</div>
<!-- ko if: $component.selectedGrantType() === 'password' && !$component.authenticated() -->
<div class="row flex flex-row">
<div class="col-4">
<label for="username" class="text-monospace form-label">Username</label>
</div>
<div class="col-6">
<div class="form-group">
<input type="text" id="username" class="form-control" data-bind="textInput: $component.username" />
</div>
</div>
</div>
<div class="row flex flex-row">
<div class="col-4">
<label for="password" class="text-monospace form-label">Password</label>
</div>
<div class="col-6">
<div class="form-group">
<input type="password" id="password" class="form-control" data-bind="textInput: $component.password" />
<span class="invalid-feedback" data-bind="text: $component.authorizationError"></span>
</div>
</div>
</div>
<div class="row flex flex-row">
<div class="col-4">
</div>
<div class="col-6">
<div class="form-group">
<button class="button button-primary" data-bind="click: $component.authenticateOAuthWithPassword">Authorize</button>
</div>
</div>
</div>
<!-- /ko -->
<!-- /ko -->



<!-- ko if: $component.subscriptionKeyRequired -->
<div class="row flex flex-row">
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import * as ko from "knockout";
import * as validation from "knockout.validation";
import template from "./operation-console.html";
import { HttpClient, HttpRequest, HttpResponse } from "@paperbits/common/http";
import { Component, Param, OnMounted } from "@paperbits/common/ko/decorators";
import { ISettingsProvider } from "@paperbits/common/configuration";
import { Operation } from "../../../../../models/operation";
Expand All @@ -16,7 +17,6 @@ import { ProductService } from "../../../../../services/productService";
import { UsersService } from "../../../../../services/usersService";
import { TenantService } from "../../../../../services/tenantService";
import { ServiceSkuName, TypeOfApi } from "../../../../../constants";
import { HttpClient, HttpRequest, HttpResponse } from "@paperbits/common/http";
import { Revision } from "../../../../../models/revision";
import { templates } from "./templates/templates";
import { ConsoleParameter } from "../../../../../models/console/consoleParameter";
Expand All @@ -27,6 +27,8 @@ import { OAuthService } from "../../../../../services/oauthService";
import { AuthorizationServer } from "../../../../../models/authorizationServer";
import { SessionManager } from "../../../../../authentication/sessionManager";
import { OAuthSession, StoredCredentials } from "./oauthSession";
import { UnauthorizedError } from "../../../../../errors/unauthorizedError";
import { GrantTypes } from "./../../../../../constants";
import { ResponsePackage } from "./responsePackage";

const oauthSessionKey = "oauthSession";
Expand Down Expand Up @@ -56,6 +58,10 @@ export class OperationConsole {
public readonly hostnameSelectionEnabled: ko.Observable<boolean>;
public readonly wildcardSegment: ko.Observable<string>;
public readonly selectedGrantType: ko.Observable<string>;
public readonly username: ko.Observable<string>;
public readonly password: ko.Observable<string>;
public readonly authorizationError: ko.Observable<string>;
public readonly authenticated: ko.Observable<boolean>;
public isConsumptionMode: boolean;
public templates: Object;
public backendUrl: string;
Expand Down Expand Up @@ -98,6 +104,11 @@ export class OperationConsole {
this.isHostnameWildcarded = ko.computed(() => this.selectedHostname().includes("*"));
this.selectedGrantType = ko.observable();
this.authorizationServer = ko.observable();
this.username = ko.observable();
this.password = ko.observable();
this.authorizationError = ko.observable();
this.authenticated = ko.observable(false);

this.useCorsProxy = ko.observable(false);
this.wildcardSegment = ko.observable();

Expand Down Expand Up @@ -153,7 +164,7 @@ export class OperationConsole {
this.api.subscribe(this.resetConsole);
this.operation.subscribe(this.resetConsole);
this.selectedLanguage.subscribe(this.updateRequestSummary);
this.selectedGrantType.subscribe(this.authenticateOAuth);
this.selectedGrantType.subscribe(this.onGrantTypeChange);
}

private async resetConsole(): Promise<void> {
Expand Down Expand Up @@ -374,6 +385,7 @@ export class OperationConsole {
private removeAuthorizationHeader(): void {
const authorizationHeader = this.findHeader(KnownHttpHeaders.Authorization);
this.removeHeader(authorizationHeader);
this.authenticated(false);
}

private setAuthorizationHeader(accessToken: string): void {
Expand All @@ -390,6 +402,7 @@ export class OperationConsole {

this.consoleOperation().request.headers.push(keyHeader);
this.updateRequestSummary();
this.authenticated(true);
}

private removeSubscriptionKeyHeader(): void {
Expand Down Expand Up @@ -625,27 +638,53 @@ export class OperationConsole {
this.removeAuthorizationHeader();
}

/**
* Initiates specified authentication flow.
* @param grantType OAuth grant type, e.g. "implicit" or "authorization_code".
*/
public async authenticateOAuth(grantType: string): Promise<void> {
public async authenticateOAuthWithPassword(): Promise<void> {
try {
this.authorizationError(null);

const api = this.api();
const authorizationServer = this.authorizationServer();
const scopeOverride = api.authenticationSettings?.oAuth2?.scope;
const serverName = authorizationServer.name;

if (scopeOverride) {
authorizationServer.scopes = [scopeOverride];
}

const accessToken = await this.oauthService.authenticatePassword(this.username(), this.password(), authorizationServer);
await this.setStoredCredentials(serverName, scopeOverride, GrantTypes.password, accessToken);

this.setAuthorizationHeader(accessToken);
}
catch (error) {
if (error instanceof UnauthorizedError) {
this.authorizationError(error.message);
return;
}

this.authorizationError("Oops, something went wrong. Try again later.");
}
}

private async onGrantTypeChange(grantType: string): Promise<void> {
await this.clearStoredCredentials();

if (!grantType) {
if (!grantType || grantType === GrantTypes.password) {
return;
}

await this.authenticateOAuth(grantType);
}

/**
* Initiates specified authentication flow.
* @param grantType OAuth grant type, e.g. "implicit" or "authorization_code".
*/
public async authenticateOAuth(grantType: string): Promise<void> {
const api = this.api();
const authorizationServer = this.authorizationServer();
const scopeOverride = api.authenticationSettings?.oAuth2?.scope;
const serverName = authorizationServer.name;
const storedCredentials = await this.getStoredCredentials(serverName, scopeOverride);

if (storedCredentials) {
this.setAuthorizationHeader(storedCredentials.accessToken);
return;
}

if (scopeOverride) {
authorizationServer.scopes = [scopeOverride];
Expand Down
8 changes: 7 additions & 1 deletion src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,13 @@ export enum GrantTypes {
* The Client Credentials grant type is used by clients to obtain an access token outside of
* the context of a user.
*/
clientCredentials = "client_credentials"
clientCredentials = "client_credentials",

/**
* The Resource owner password grant type is used to exchange a username and password for an access
* token directly.
*/
password = "password"
}

export const managementApiVersion = "2019-12-01";
Expand Down
13 changes: 13 additions & 0 deletions src/errors/unauthorizedError.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
export class UnauthorizedError extends Error {
constructor(
public readonly message: string,
public readonly innerError?: Error
) {
super();
Object.setPrototypeOf(this, UnauthorizedError.prototype);
}

public toString(): string {
return `${this.stack} `;
}
}
2 changes: 1 addition & 1 deletion src/services/mapiClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,7 @@ export class MapiClient {
const text = response.toText();

if (response.statusCode >= 200 && response.statusCode < 300) {
if ((contentType.includes("json")) && text.length > 0) {
if (contentType.includes("json") && text.length > 0) {
return JSON.parse(text) as T;
}
else {
Expand Down
29 changes: 27 additions & 2 deletions src/services/oauthService.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { UnauthorizedError } from "./../errors/unauthorizedError";
import * as ClientOAuth2 from "client-oauth2";
import * as Utils from "@paperbits/common";
import { HttpClient } from "@paperbits/common/http";
import { HttpClient, HttpMethod } from "@paperbits/common/http";
import { ISettingsProvider } from "@paperbits/common/configuration";
import { GrantTypes } from "./../constants";
import { MapiClient } from "./mapiClient";
Expand Down Expand Up @@ -88,7 +89,7 @@ export class OAuthService {
* @param backendUrl {string} Portal backend URL.
* @param authorizationServer {AuthorizationServer} Authorization server details.
*/
public authenticateImplicit(backendUrl: string, authorizationServer: AuthorizationServer): Promise<string> {
public authenticateImplicit(backendUrl: string, authorizationServer: AuthorizationServer): Promise<string> {
const redirectUri = `${backendUrl}/signin-oauth/implicit/callback`;
const query = {
state: Utils.guid()
Expand Down Expand Up @@ -215,6 +216,30 @@ export class OAuthService {
});
}

public async authenticatePassword(username: string, password: string, authorizationServer: AuthorizationServer): Promise<string> {
const backendUrl = await this.settingsProvider.getSetting<string>("backendUrl") || `https://${location.hostname}`;
let uri = `${backendUrl}/signin-oauth/password/${authorizationServer.name}`;

if (authorizationServer.scopes) {
const scopesString = authorizationServer.scopes.join(" ");
uri += `?scopes=${encodeURIComponent(scopesString)}`;
}

const response = await this.httpClient.send<any>({
method: HttpMethod.post,
url: uri,
body: JSON.stringify({ username: username, password: password })
});

if (response.statusCode === 401) {
throw new UnauthorizedError("Unable to authenticate. Verify the credentials you entered are correct.");
}

const tokenInfo = response.toObject();

return `${tokenInfo.accessTokenType} ${tokenInfo.accessToken}`;
}

public async discoverOAuthServer(metadataEndpoint: string): Promise<AuthorizationServer> {
const response = await this.httpClient.send<OpenIdConnectMetadata>({ url: metadataEndpoint });
const metadata = response.toObject();
Expand Down
2 changes: 1 addition & 1 deletion webpack.designer.js
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ const designerConfig = {
{ from: `./src/themes/designer/assets/index.html`, to: "index.html" },
{ from: `./src/themes/designer/styles/fonts`, to: "editors/styles/fonts" },
{ from: `./src/libraries`, to: "data" },
{ from: `./scripts/data.json`, to: "editors/themes/default.json" },
{ from: `./scripts.v3/data.json`, to: "editors/themes/default.json" },
{ from: "./src/config.design.json", to: "config.json" }
]
})
Expand Down

0 comments on commit 40839a0

Please sign in to comment.