From 11ed6342f6de23149580a9c01523f4f1a50ebabf Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Wed, 13 Mar 2019 00:28:12 -0600 Subject: [PATCH] First iteration of OpenID reauth widget example --- web/app/app.module.ts | 2 + web/app/app.routing.ts | 2 + .../shared/models/scalar-server-responses.ts | 11 ++ .../shared/models/scalar-widget-actions.ts | 11 +- .../scalar/scalar-server-api.service.ts | 10 +- .../services/scalar/scalar-widget.api.ts | 21 +++- .../reauth-example.component.html | 14 +++ .../reauth-example.component.scss | 4 + .../reauth-example.component.ts | 119 ++++++++++++++++++ 9 files changed, 191 insertions(+), 3 deletions(-) create mode 100644 web/app/widget-wrappers/reauth-example/reauth-example.component.html create mode 100644 web/app/widget-wrappers/reauth-example/reauth-example.component.scss create mode 100644 web/app/widget-wrappers/reauth-example/reauth-example.component.ts diff --git a/web/app/app.module.ts b/web/app/app.module.ts index deecc52..9464dd9 100644 --- a/web/app/app.module.ts +++ b/web/app/app.module.ts @@ -109,6 +109,7 @@ import { SlackBridgeConfigComponent } from "./configs/bridge/slack/slack.bridge. import { AdminSlackBridgeManageSelfhostedComponent } from "./admin/bridges/slack/manage-selfhosted/manage-selfhosted.component"; import { AdminSlackBridgeComponent } from "./admin/bridges/slack/slack.component"; import { AdminSlackApiService } from "./shared/services/admin/admin-slack-api.service"; +import { ReauthExampleWidgetWrapperComponent } from "./widget-wrappers/reauth-example/reauth-example.component"; @NgModule({ imports: [ @@ -198,6 +199,7 @@ import { AdminSlackApiService } from "./shared/services/admin/admin-slack-api.se SlackBridgeConfigComponent, AdminSlackBridgeManageSelfhostedComponent, AdminSlackBridgeComponent, + ReauthExampleWidgetWrapperComponent, // Vendor ], diff --git a/web/app/app.routing.ts b/web/app/app.routing.ts index db55507..9c5a578 100644 --- a/web/app/app.routing.ts +++ b/web/app/app.routing.ts @@ -42,6 +42,7 @@ import { SpotifyWidgetWrapperComponent } from "./widget-wrappers/spotify/spotify import { AdminCustomBotsComponent } from "./admin/custom-bots/custom-bots.component"; import { AdminSlackBridgeComponent } from "./admin/bridges/slack/slack.component"; import { SlackBridgeConfigComponent } from "./configs/bridge/slack/slack.bridge.component"; +import { ReauthExampleWidgetWrapperComponent } from "./widget-wrappers/reauth-example/reauth-example.component"; const routes: Routes = [ {path: "", component: HomeComponent}, @@ -264,6 +265,7 @@ const routes: Routes = [ {path: "generic-fullscreen", component: GenericFullscreenWidgetWrapperComponent}, {path: "tradingview", component: TradingViewWidgetWrapperComponent}, {path: "spotify", component: SpotifyWidgetWrapperComponent}, + {path: "reauth", component: ReauthExampleWidgetWrapperComponent}, ] }, ]; diff --git a/web/app/shared/models/scalar-server-responses.ts b/web/app/shared/models/scalar-server-responses.ts index dbfa945..11f9b62 100644 --- a/web/app/shared/models/scalar-server-responses.ts +++ b/web/app/shared/models/scalar-server-responses.ts @@ -1,3 +1,14 @@ export interface FE_ScalarAccountResponse { user_id: string; +} + +export interface FE_ScalarRegisterResponse { + scalar_token: string; +} + +export interface FE_ScalarOpenIdRequestBody { + access_token: string; + expires_in: number; + matrix_server_name: string; + token_type: string; } \ No newline at end of file diff --git a/web/app/shared/models/scalar-widget-actions.ts b/web/app/shared/models/scalar-widget-actions.ts index 7764014..eed7899 100644 --- a/web/app/shared/models/scalar-widget-actions.ts +++ b/web/app/shared/models/scalar-widget-actions.ts @@ -1,7 +1,16 @@ export interface ScalarToWidgetRequest { api: "to_widget"; - action: "supported_api_versions" | "screenshot" | "capabilities" | "send_event" | "visibility" | string; + action: "supported_api_versions" | "screenshot" | "capabilities" | "send_event" | "visibility" | "openid_credentials" | string; requestId: string; widgetId: string; data?: any; +} + +export interface ScalarFromWidgetResponse { + api: "from_widget"; + action: "get_openid" | string; + requestId: string; + widgetId: string; + data?: any; + response: any; } \ No newline at end of file diff --git a/web/app/shared/services/scalar/scalar-server-api.service.ts b/web/app/shared/services/scalar/scalar-server-api.service.ts index 765e709..cc90dde 100644 --- a/web/app/shared/services/scalar/scalar-server-api.service.ts +++ b/web/app/shared/services/scalar/scalar-server-api.service.ts @@ -1,6 +1,10 @@ import { Injectable } from "@angular/core"; import { Http } from "@angular/http"; -import { FE_ScalarAccountResponse } from "../../models/scalar-server-responses"; +import { + FE_ScalarAccountResponse, + FE_ScalarOpenIdRequestBody, + FE_ScalarRegisterResponse +} from "../../models/scalar-server-responses"; import { AuthedApi } from "../authed-api"; @Injectable() @@ -12,4 +16,8 @@ export class ScalarServerApiService extends AuthedApi { public getAccount(): Promise { return this.authedGet("/api/v1/scalar/account").map(res => res.json()).toPromise(); } + + public register(openId: FE_ScalarOpenIdRequestBody): Promise { + return this.http.post("/api/v1/scalar/register", openId).map(res => res.json()).toPromise(); + } } diff --git a/web/app/shared/services/scalar/scalar-widget.api.ts b/web/app/shared/services/scalar/scalar-widget.api.ts index b4f90f4..3267145 100644 --- a/web/app/shared/services/scalar/scalar-widget.api.ts +++ b/web/app/shared/services/scalar/scalar-widget.api.ts @@ -1,4 +1,4 @@ -import { ScalarToWidgetRequest } from "../../models/scalar-widget-actions"; +import { ScalarFromWidgetResponse, ScalarToWidgetRequest } from "../../models/scalar-widget-actions"; import { ReplaySubject } from "rxjs/ReplaySubject"; import { Subject } from "rxjs/Subject"; import { FE_Sticker, FE_StickerPack } from "../../models/integration"; @@ -7,7 +7,9 @@ import * as randomString from "random-string"; export class ScalarWidgetApi { public static requestReceived: Subject = new ReplaySubject(); + public static replyReceived: Subject = new ReplaySubject(); public static widgetId: string; + public static inFlightRequestIds: string[] = []; private constructor() { } @@ -82,6 +84,10 @@ export class ScalarWidgetApi { }); } + public static requestOpenID(): void { + ScalarWidgetApi.callAction("get_openid", {}); + } + private static callAction(action, payload) { if (!window.opener) { return; @@ -93,6 +99,8 @@ export class ScalarWidgetApi { request["action"] = action; request["requestId"] = randomString({length: 160}); + ScalarWidgetApi.inFlightRequestIds.push(request["requestId"]); + console.log("[Dimension] Sending fromWidget: ", request); window.opener.postMessage(request, "*"); } @@ -120,4 +128,15 @@ window.addEventListener("message", event => { ScalarWidgetApi.requestReceived.next(event.data); return; } + + if (event.data.api === "fromWidget" && ScalarWidgetApi.inFlightRequestIds.indexOf(event.data.requestId) !== -1) { + if (event.data.widgetId && !ScalarWidgetApi.widgetId) ScalarWidgetApi.widgetId = event.data.widgetId; + console.log(`[Dimension] Received fromWidget response at widget ID ${ScalarWidgetApi.widgetId}: ${JSON.stringify(event.data)}`); + + const idx = ScalarWidgetApi.inFlightRequestIds.indexOf(event.data.requestId); + ScalarWidgetApi.inFlightRequestIds.splice(idx, 1); + ScalarWidgetApi.replyReceived.next(event.data); + + return; + } }); diff --git a/web/app/widget-wrappers/reauth-example/reauth-example.component.html b/web/app/widget-wrappers/reauth-example/reauth-example.component.html new file mode 100644 index 0000000..0b21bfe --- /dev/null +++ b/web/app/widget-wrappers/reauth-example/reauth-example.component.html @@ -0,0 +1,14 @@ +
+ +
+
+

User ID: {{userId}}

+
+
+

{{stateMessage}}

+

You have blocked this widget from receiving credentials.

+

An error has occurred - see logs for details

+ Widget ID: {{widgetId}} +
\ No newline at end of file diff --git a/web/app/widget-wrappers/reauth-example/reauth-example.component.scss b/web/app/widget-wrappers/reauth-example/reauth-example.component.scss new file mode 100644 index 0000000..3f753cc --- /dev/null +++ b/web/app/widget-wrappers/reauth-example/reauth-example.component.scss @@ -0,0 +1,4 @@ +.reauth-container { + padding-top: 20px; + text-align: center; +} diff --git a/web/app/widget-wrappers/reauth-example/reauth-example.component.ts b/web/app/widget-wrappers/reauth-example/reauth-example.component.ts new file mode 100644 index 0000000..dcdd98d --- /dev/null +++ b/web/app/widget-wrappers/reauth-example/reauth-example.component.ts @@ -0,0 +1,119 @@ +import { ChangeDetectorRef, Component, OnDestroy, OnInit } from "@angular/core"; +import { ScalarWidgetApi } from "../../shared/services/scalar/scalar-widget.api"; +import { Subscription } from "rxjs"; +import { CapableWidget } from "../capable-widget"; +import { ActivatedRoute } from "@angular/router"; +import { ScalarServerApiService } from "../../shared/services/scalar/scalar-server-api.service"; +import { SessionStorage } from "../../shared/SessionStorage"; +import { FE_ScalarOpenIdRequestBody } from "../../shared/models/scalar-server-responses"; + +@Component({ + selector: "my-reauth-example-widget-wrapper", + templateUrl: "reauth-example.component.html", + styleUrls: ["reauth-example.component.scss"], +}) +export class ReauthExampleWidgetWrapperComponent extends CapableWidget implements OnInit, OnDestroy { + + public busy = false; + public hasOpenId = false; + public userId: string; + public blocked = false; + public error = false; + public stateMessage = ""; + + private widgetReplySubscription: Subscription; + private widgetRequestSubscription: Subscription; + + constructor(activatedRoute: ActivatedRoute, + private scalarApi: ScalarServerApiService, + private changeDetector: ChangeDetectorRef) { + super(); + + const params: any = activatedRoute.snapshot.queryParams; + ScalarWidgetApi.widgetId = params.widgetId; + } + + public get widgetId(): string { + return ScalarWidgetApi.widgetId; + } + + public ngOnInit(): void { + // TODO: Check that the client supports this (API version 0.0.2 at least) + + super.ngOnInit(); + this.widgetReplySubscription = ScalarWidgetApi.replyReceived.subscribe(async response => { + if (response.action !== "get_openid") return; + + const data = response.response; + + try { + if (data.state === "request") { + this.stateMessage = "Waiting for you to accept the prompt..."; + } else if (data.state === "allowed") { + await this.exchangeOpenIdInfo(data); + } else { + this.blocked = true; + this.busy = false; + this.hasOpenId = false; + this.stateMessage = null; + } + } catch (e) { + console.error(e); + this.error = true; + this.busy = false; + this.stateMessage = null; + } + + this.changeDetector.detectChanges(); + }); + + this.widgetRequestSubscription = ScalarWidgetApi.requestReceived.subscribe(async request => { + if (request.action !== "openid_credentials") return; + ScalarWidgetApi.replyAcknowledge(request); + + try { + if (request.data.success) { + await this.exchangeOpenIdInfo(request.data); + } else { + this.blocked = true; + this.busy = false; + this.stateMessage = null; + } + } catch (e) { + console.error(e); + this.error = true; + this.busy = false; + this.stateMessage = null; + } + + this.changeDetector.detectChanges(); + }); + } + + public ngOnDestroy() { + super.ngOnDestroy(); + if (this.widgetReplySubscription) this.widgetReplySubscription.unsubscribe(); + if (this.widgetRequestSubscription) this.widgetRequestSubscription.unsubscribe(); + } + + public onReauthStart(): void { + this.busy = true; + this.error = false; + this.blocked = false; + this.hasOpenId = false; + ScalarWidgetApi.requestOpenID(); + } + + private async exchangeOpenIdInfo(openId: FE_ScalarOpenIdRequestBody) { + this.stateMessage = "Exchanging OpenID credentials for token..."; + this.changeDetector.detectChanges(); + const scalarTokenResp = await this.scalarApi.register(openId); + SessionStorage.scalarToken = scalarTokenResp.scalar_token; + const userInfo = await this.scalarApi.getAccount(); + this.hasOpenId = true; + this.userId = userInfo.user_id; + this.blocked = false; + this.busy = false; + this.stateMessage = null; + } +}