Skip to content

Commit 1f64317

Browse files
authored
Added support for "resource owner password" grant flow. (#1219)
1 parent c4b5379 commit 1f64317

File tree

6 files changed

+129
-18
lines changed

6 files changed

+129
-18
lines changed

src/components/operations/operation-details/ko/runtime/operation-console.html

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,41 @@ <h3>Authorization</h3>
8787
</div>
8888
</div>
8989
</div>
90+
<!-- ko if: $component.selectedGrantType() === 'password' && !$component.authenticated() -->
91+
<div class="row flex flex-row">
92+
<div class="col-4">
93+
<label for="username" class="text-monospace form-label">Username</label>
94+
</div>
95+
<div class="col-6">
96+
<div class="form-group">
97+
<input type="text" id="username" class="form-control" data-bind="textInput: $component.username" />
98+
</div>
99+
</div>
100+
</div>
101+
<div class="row flex flex-row">
102+
<div class="col-4">
103+
<label for="password" class="text-monospace form-label">Password</label>
104+
</div>
105+
<div class="col-6">
106+
<div class="form-group">
107+
<input type="password" id="password" class="form-control" data-bind="textInput: $component.password" />
108+
<span class="invalid-feedback" data-bind="text: $component.authorizationError"></span>
109+
</div>
110+
</div>
111+
</div>
112+
<div class="row flex flex-row">
113+
<div class="col-4">
114+
</div>
115+
<div class="col-6">
116+
<div class="form-group">
117+
<button class="button button-primary" data-bind="click: $component.authenticateOAuthWithPassword">Authorize</button>
118+
</div>
119+
</div>
120+
</div>
90121
<!-- /ko -->
122+
<!-- /ko -->
123+
124+
91125

92126
<!-- ko if: $component.subscriptionKeyRequired -->
93127
<div class="row flex flex-row">

src/components/operations/operation-details/ko/runtime/operation-console.ts

Lines changed: 53 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import * as ko from "knockout";
22
import * as validation from "knockout.validation";
33
import template from "./operation-console.html";
4+
import { HttpClient, HttpRequest, HttpResponse } from "@paperbits/common/http";
45
import { Component, Param, OnMounted } from "@paperbits/common/ko/decorators";
56
import { ISettingsProvider } from "@paperbits/common/configuration";
67
import { Operation } from "../../../../../models/operation";
@@ -16,7 +17,6 @@ import { ProductService } from "../../../../../services/productService";
1617
import { UsersService } from "../../../../../services/usersService";
1718
import { TenantService } from "../../../../../services/tenantService";
1819
import { ServiceSkuName, TypeOfApi } from "../../../../../constants";
19-
import { HttpClient, HttpRequest, HttpResponse } from "@paperbits/common/http";
2020
import { Revision } from "../../../../../models/revision";
2121
import { templates } from "./templates/templates";
2222
import { ConsoleParameter } from "../../../../../models/console/consoleParameter";
@@ -27,6 +27,8 @@ import { OAuthService } from "../../../../../services/oauthService";
2727
import { AuthorizationServer } from "../../../../../models/authorizationServer";
2828
import { SessionManager } from "../../../../../authentication/sessionManager";
2929
import { OAuthSession, StoredCredentials } from "./oauthSession";
30+
import { UnauthorizedError } from "../../../../../errors/unauthorizedError";
31+
import { GrantTypes } from "./../../../../../constants";
3032
import { ResponsePackage } from "./responsePackage";
3133

3234
const oauthSessionKey = "oauthSession";
@@ -56,6 +58,10 @@ export class OperationConsole {
5658
public readonly hostnameSelectionEnabled: ko.Observable<boolean>;
5759
public readonly wildcardSegment: ko.Observable<string>;
5860
public readonly selectedGrantType: ko.Observable<string>;
61+
public readonly username: ko.Observable<string>;
62+
public readonly password: ko.Observable<string>;
63+
public readonly authorizationError: ko.Observable<string>;
64+
public readonly authenticated: ko.Observable<boolean>;
5965
public isConsumptionMode: boolean;
6066
public templates: Object;
6167
public backendUrl: string;
@@ -98,6 +104,11 @@ export class OperationConsole {
98104
this.isHostnameWildcarded = ko.computed(() => this.selectedHostname().includes("*"));
99105
this.selectedGrantType = ko.observable();
100106
this.authorizationServer = ko.observable();
107+
this.username = ko.observable();
108+
this.password = ko.observable();
109+
this.authorizationError = ko.observable();
110+
this.authenticated = ko.observable(false);
111+
101112
this.useCorsProxy = ko.observable(false);
102113
this.wildcardSegment = ko.observable();
103114

@@ -153,7 +164,7 @@ export class OperationConsole {
153164
this.api.subscribe(this.resetConsole);
154165
this.operation.subscribe(this.resetConsole);
155166
this.selectedLanguage.subscribe(this.updateRequestSummary);
156-
this.selectedGrantType.subscribe(this.authenticateOAuth);
167+
this.selectedGrantType.subscribe(this.onGrantTypeChange);
157168
}
158169

159170
private async resetConsole(): Promise<void> {
@@ -374,6 +385,7 @@ export class OperationConsole {
374385
private removeAuthorizationHeader(): void {
375386
const authorizationHeader = this.findHeader(KnownHttpHeaders.Authorization);
376387
this.removeHeader(authorizationHeader);
388+
this.authenticated(false);
377389
}
378390

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

391403
this.consoleOperation().request.headers.push(keyHeader);
392404
this.updateRequestSummary();
405+
this.authenticated(true);
393406
}
394407

395408
private removeSubscriptionKeyHeader(): void {
@@ -625,27 +638,53 @@ export class OperationConsole {
625638
this.removeAuthorizationHeader();
626639
}
627640

628-
/**
629-
* Initiates specified authentication flow.
630-
* @param grantType OAuth grant type, e.g. "implicit" or "authorization_code".
631-
*/
632-
public async authenticateOAuth(grantType: string): Promise<void> {
641+
public async authenticateOAuthWithPassword(): Promise<void> {
642+
try {
643+
this.authorizationError(null);
644+
645+
const api = this.api();
646+
const authorizationServer = this.authorizationServer();
647+
const scopeOverride = api.authenticationSettings?.oAuth2?.scope;
648+
const serverName = authorizationServer.name;
649+
650+
if (scopeOverride) {
651+
authorizationServer.scopes = [scopeOverride];
652+
}
653+
654+
const accessToken = await this.oauthService.authenticatePassword(this.username(), this.password(), authorizationServer);
655+
await this.setStoredCredentials(serverName, scopeOverride, GrantTypes.password, accessToken);
656+
657+
this.setAuthorizationHeader(accessToken);
658+
}
659+
catch (error) {
660+
if (error instanceof UnauthorizedError) {
661+
this.authorizationError(error.message);
662+
return;
663+
}
664+
665+
this.authorizationError("Oops, something went wrong. Try again later.");
666+
}
667+
}
668+
669+
private async onGrantTypeChange(grantType: string): Promise<void> {
633670
await this.clearStoredCredentials();
634671

635-
if (!grantType) {
672+
if (!grantType || grantType === GrantTypes.password) {
636673
return;
637674
}
638675

676+
await this.authenticateOAuth(grantType);
677+
}
678+
679+
/**
680+
* Initiates specified authentication flow.
681+
* @param grantType OAuth grant type, e.g. "implicit" or "authorization_code".
682+
*/
683+
public async authenticateOAuth(grantType: string): Promise<void> {
639684
const api = this.api();
640685
const authorizationServer = this.authorizationServer();
641686
const scopeOverride = api.authenticationSettings?.oAuth2?.scope;
642687
const serverName = authorizationServer.name;
643-
const storedCredentials = await this.getStoredCredentials(serverName, scopeOverride);
644-
645-
if (storedCredentials) {
646-
this.setAuthorizationHeader(storedCredentials.accessToken);
647-
return;
648-
}
649688

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

src/errors/unauthorizedError.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
export class UnauthorizedError extends Error {
2+
constructor(
3+
public readonly message: string,
4+
public readonly innerError?: Error
5+
) {
6+
super();
7+
Object.setPrototypeOf(this, UnauthorizedError.prototype);
8+
}
9+
10+
public toString(): string {
11+
return `${this.stack} `;
12+
}
13+
}

src/services/mapiClient.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -159,7 +159,7 @@ export class MapiClient {
159159
const text = response.toText();
160160

161161
if (response.statusCode >= 200 && response.statusCode < 300) {
162-
if ((contentType.includes("json")) && text.length > 0) {
162+
if (contentType.includes("json") && text.length > 0) {
163163
return JSON.parse(text) as T;
164164
}
165165
else {

src/services/oauthService.ts

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1+
import { UnauthorizedError } from "./../errors/unauthorizedError";
12
import * as ClientOAuth2 from "client-oauth2";
23
import * as Utils from "@paperbits/common";
3-
import { HttpClient } from "@paperbits/common/http";
4+
import { HttpClient, HttpMethod } from "@paperbits/common/http";
45
import { ISettingsProvider } from "@paperbits/common/configuration";
56
import { GrantTypes } from "./../constants";
67
import { MapiClient } from "./mapiClient";
@@ -88,7 +89,7 @@ export class OAuthService {
8889
* @param backendUrl {string} Portal backend URL.
8990
* @param authorizationServer {AuthorizationServer} Authorization server details.
9091
*/
91-
public authenticateImplicit(backendUrl: string, authorizationServer: AuthorizationServer): Promise<string> {
92+
public authenticateImplicit(backendUrl: string, authorizationServer: AuthorizationServer): Promise<string> {
9293
const redirectUri = `${backendUrl}/signin-oauth/implicit/callback`;
9394
const query = {
9495
state: Utils.guid()
@@ -215,6 +216,30 @@ export class OAuthService {
215216
});
216217
}
217218

219+
public async authenticatePassword(username: string, password: string, authorizationServer: AuthorizationServer): Promise<string> {
220+
const backendUrl = await this.settingsProvider.getSetting<string>("backendUrl") || `https://${location.hostname}`;
221+
let uri = `${backendUrl}/signin-oauth/password/${authorizationServer.name}`;
222+
223+
if (authorizationServer.scopes) {
224+
const scopesString = authorizationServer.scopes.join(" ");
225+
uri += `?scopes=${encodeURIComponent(scopesString)}`;
226+
}
227+
228+
const response = await this.httpClient.send<any>({
229+
method: HttpMethod.post,
230+
url: uri,
231+
body: JSON.stringify({ username: username, password: password })
232+
});
233+
234+
if (response.statusCode === 401) {
235+
throw new UnauthorizedError("Unable to authenticate. Verify the credentials you entered are correct.");
236+
}
237+
238+
const tokenInfo = response.toObject();
239+
240+
return `${tokenInfo.accessTokenType} ${tokenInfo.accessToken}`;
241+
}
242+
218243
public async discoverOAuthServer(metadataEndpoint: string): Promise<AuthorizationServer> {
219244
const response = await this.httpClient.send<OpenIdConnectMetadata>({ url: metadataEndpoint });
220245
const metadata = response.toObject();

webpack.designer.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ const designerConfig = {
6666
{ from: `./src/themes/designer/assets/index.html`, to: "index.html" },
6767
{ from: `./src/themes/designer/styles/fonts`, to: "editors/styles/fonts" },
6868
{ from: `./src/libraries`, to: "data" },
69-
{ from: `./scripts/data.json`, to: "editors/themes/default.json" },
69+
{ from: `./scripts.v3/data.json`, to: "editors/themes/default.json" },
7070
{ from: "./src/config.design.json", to: "config.json" }
7171
]
7272
})

0 commit comments

Comments
 (0)