From 70a142b84d01e89b56d027b24e37122fa90fe25b Mon Sep 17 00:00:00 2001 From: Sam Wilkins Date: Wed, 9 Oct 2019 16:53:16 -0400 Subject: better authentication feedback, cleanup --- src/client/apis/GoogleAuthenticationManager.tsx | 114 ++++++++++++++++++++++++ 1 file changed, 114 insertions(+) create mode 100644 src/client/apis/GoogleAuthenticationManager.tsx (limited to 'src/client/apis/GoogleAuthenticationManager.tsx') diff --git a/src/client/apis/GoogleAuthenticationManager.tsx b/src/client/apis/GoogleAuthenticationManager.tsx new file mode 100644 index 000000000..1ab6380ef --- /dev/null +++ b/src/client/apis/GoogleAuthenticationManager.tsx @@ -0,0 +1,114 @@ +import { observable, action, reaction, runInAction } from "mobx"; +import { observer } from "mobx-react"; +import * as React from "react"; +import MainViewModal from "../views/MainViewModal"; +import { Opt } from "../../new_fields/Doc"; +import { Identified } from "../Network"; +import { RouteStore } from "../../server/RouteStore"; +import "./GoogleAuthenticationManager.scss"; + +const AuthenticationUrl = "https://accounts.google.com/o/oauth2/v2/auth"; +const prompt = "Paste authorization code here..."; + +@observer +export default class GoogleAuthenticationManager extends React.Component<{}> { + public static Instance: GoogleAuthenticationManager; + @observable private openState = false; + private authenticationLink: Opt = undefined; + @observable private authenticationCode: Opt = undefined; + @observable private clickedState = false; + @observable private success: Opt = undefined; + @observable private displayLauncher = true; + + private set isOpen(value: boolean) { + runInAction(() => this.openState = value); + } + + private set hasBeenClicked(value: boolean) { + runInAction(() => this.clickedState = value); + } + + public fetchOrGenerateAccessToken = async () => { + let response = await Identified.FetchFromServer(RouteStore.readGoogleAccessToken); + // if this is an authentication url, activate the UI to register the new access token + if (new RegExp(AuthenticationUrl).test(response)) { + this.isOpen = true; + this.authenticationLink = response; + return new Promise(async resolve => { + const disposer = reaction( + () => this.authenticationCode, + authenticationCode => { + if (authenticationCode) { + Identified.PostToServer(RouteStore.writeGoogleAccessToken, { authenticationCode }).then( + token => { + runInAction(() => this.success = true); + setTimeout(() => { + this.isOpen = false; + runInAction(() => this.displayLauncher = false); + setTimeout(() => { + runInAction(() => this.success = undefined); + runInAction(() => this.displayLauncher = true); + this.hasBeenClicked = false; + }, 500); + }, 1000); + disposer(); + resolve(token); + }, + () => { + this.hasBeenClicked = false; + runInAction(() => this.success = false); + } + ); + } + } + ); + }); + } + // otherwise, we already have a valid, stored access token + return response; + } + + constructor(props: {}) { + super(props); + GoogleAuthenticationManager.Instance = this; + } + + private handleClick = () => { + window.open(this.authenticationLink); + setTimeout(() => this.hasBeenClicked = true, 500); + } + + private handlePaste = action((e: React.ChangeEvent) => { + this.authenticationCode = e.currentTarget.value; + }); + + private get renderPrompt() { + return ( +
+ {this.displayLauncher ? : (null)} + {this.clickedState ? : (null)} +
+ ); + } + + render() { + return ( + + ); + } + +} \ No newline at end of file -- cgit v1.2.3-70-g09d2 From 0890639c166df6558c3a5c0fd3d427157b3549de Mon Sep 17 00:00:00 2001 From: Sam Wilkins Date: Wed, 9 Oct 2019 16:53:57 -0400 Subject: margin fix --- src/client/apis/GoogleAuthenticationManager.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src/client/apis/GoogleAuthenticationManager.tsx') diff --git a/src/client/apis/GoogleAuthenticationManager.tsx b/src/client/apis/GoogleAuthenticationManager.tsx index 1ab6380ef..8e8bf8def 100644 --- a/src/client/apis/GoogleAuthenticationManager.tsx +++ b/src/client/apis/GoogleAuthenticationManager.tsx @@ -88,12 +88,12 @@ export default class GoogleAuthenticationManager extends React.Component<{}> { {this.displayLauncher ? : (null)} {this.clickedState ? : (null)} ); -- cgit v1.2.3-70-g09d2 From e5e06155af44f86c7204ed5ce904f705186401db Mon Sep 17 00:00:00 2001 From: Sam Wilkins Date: Wed, 9 Oct 2019 17:16:02 -0400 Subject: quick margin fix --- src/client/apis/GoogleAuthenticationManager.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src/client/apis/GoogleAuthenticationManager.tsx') diff --git a/src/client/apis/GoogleAuthenticationManager.tsx b/src/client/apis/GoogleAuthenticationManager.tsx index 8e8bf8def..db108ad94 100644 --- a/src/client/apis/GoogleAuthenticationManager.tsx +++ b/src/client/apis/GoogleAuthenticationManager.tsx @@ -88,7 +88,7 @@ export default class GoogleAuthenticationManager extends React.Component<{}> { {this.displayLauncher ? : (null)} {this.clickedState ? Date: Wed, 9 Oct 2019 23:21:17 -0400 Subject: cleanup --- src/client/apis/GoogleAuthenticationManager.tsx | 37 +++++++++++++++---------- 1 file changed, 23 insertions(+), 14 deletions(-) (limited to 'src/client/apis/GoogleAuthenticationManager.tsx') diff --git a/src/client/apis/GoogleAuthenticationManager.tsx b/src/client/apis/GoogleAuthenticationManager.tsx index db108ad94..d143d8273 100644 --- a/src/client/apis/GoogleAuthenticationManager.tsx +++ b/src/client/apis/GoogleAuthenticationManager.tsx @@ -41,23 +41,14 @@ export default class GoogleAuthenticationManager extends React.Component<{}> { if (authenticationCode) { Identified.PostToServer(RouteStore.writeGoogleAccessToken, { authenticationCode }).then( token => { - runInAction(() => this.success = true); - setTimeout(() => { - this.isOpen = false; - runInAction(() => this.displayLauncher = false); - setTimeout(() => { - runInAction(() => this.success = undefined); - runInAction(() => this.displayLauncher = true); - this.hasBeenClicked = false; - }, 500); - }, 1000); + this.beginFadeout(); disposer(); resolve(token); }, - () => { + action(() => { this.hasBeenClicked = false; - runInAction(() => this.success = false); - } + this.success = false; + }) ); } } @@ -68,6 +59,19 @@ export default class GoogleAuthenticationManager extends React.Component<{}> { return response; } + beginFadeout = action(() => { + this.success = true; + setTimeout(action(() => { + this.isOpen = false; + this.displayLauncher = false; + setTimeout(action(() => { + this.success = undefined; + this.displayLauncher = true; + this.hasBeenClicked = false; + }), 500); + }), 2000); + }); + constructor(props: {}) { super(props); GoogleAuthenticationManager.Instance = this; @@ -99,6 +103,11 @@ export default class GoogleAuthenticationManager extends React.Component<{}> { ); } + private get dialogueBoxStyle() { + const borderColor = this.success === undefined ? "black" : this.success ? "green" : "red"; + return { borderColor, transition: "0.2s borderColor ease" }; + } + render() { return ( { interactive={true} contents={this.renderPrompt} overlayDisplayedOpacity={0.9} - dialogueBoxStyle={{ borderColor: this.success === undefined ? "black" : this.success ? "green" : "red" }} + dialogueBoxStyle={this.dialogueBoxStyle} /> ); } -- cgit v1.2.3-70-g09d2 From 5b83d8da6c262897dc75ada26f08ed1c46ceb95c Mon Sep 17 00:00:00 2001 From: Sam Wilkins Date: Sat, 12 Oct 2019 19:13:25 -0400 Subject: parse google user data and use it to provide customized feedback on login --- src/client/apis/GoogleAuthenticationManager.scss | 20 +++++++++- src/client/apis/GoogleAuthenticationManager.tsx | 29 +++++++++++--- src/server/apis/google/GoogleApiServerUtils.ts | 51 +++++++++++++++++++++--- src/server/database.ts | 5 ++- src/server/index.ts | 3 +- 5 files changed, 91 insertions(+), 17 deletions(-) (limited to 'src/client/apis/GoogleAuthenticationManager.tsx') diff --git a/src/client/apis/GoogleAuthenticationManager.scss b/src/client/apis/GoogleAuthenticationManager.scss index 5efb3ab3b..13bde822d 100644 --- a/src/client/apis/GoogleAuthenticationManager.scss +++ b/src/client/apis/GoogleAuthenticationManager.scss @@ -1,3 +1,19 @@ -.paste-target { - padding: 5px; +.authorize-container { + display: flex; + flex-direction: column; + align-items: center; + + .paste-target { + padding: 5px; + width: 100%; + } + + .avatar { + border-radius: 50%; + } + + .welcome { + font-style: italic; + margin-top: 15px; + } } \ No newline at end of file diff --git a/src/client/apis/GoogleAuthenticationManager.tsx b/src/client/apis/GoogleAuthenticationManager.tsx index d143d8273..01dac3996 100644 --- a/src/client/apis/GoogleAuthenticationManager.tsx +++ b/src/client/apis/GoogleAuthenticationManager.tsx @@ -19,6 +19,8 @@ export default class GoogleAuthenticationManager extends React.Component<{}> { @observable private clickedState = false; @observable private success: Opt = undefined; @observable private displayLauncher = true; + @observable private avatar: Opt = undefined; + @observable private username: Opt = undefined; private set isOpen(value: boolean) { runInAction(() => this.openState = value); @@ -40,10 +42,14 @@ export default class GoogleAuthenticationManager extends React.Component<{}> { authenticationCode => { if (authenticationCode) { Identified.PostToServer(RouteStore.writeGoogleAccessToken, { authenticationCode }).then( - token => { + ({ access_token, avatar, name }) => { + runInAction(() => { + this.avatar = avatar; + this.username = name; + }); this.beginFadeout(); disposer(); - resolve(token); + resolve(access_token); }, action(() => { this.hasBeenClicked = false; @@ -61,15 +67,18 @@ export default class GoogleAuthenticationManager extends React.Component<{}> { beginFadeout = action(() => { this.success = true; + this.authenticationCode = undefined; + this.displayLauncher = false; + this.hasBeenClicked = false; setTimeout(action(() => { this.isOpen = false; - this.displayLauncher = false; setTimeout(action(() => { this.success = undefined; this.displayLauncher = true; - this.hasBeenClicked = false; + this.avatar = undefined; + this.username = undefined; }), 500); - }), 2000); + }), 3000); }); constructor(props: {}) { @@ -88,7 +97,7 @@ export default class GoogleAuthenticationManager extends React.Component<{}> { private get renderPrompt() { return ( -
+
{this.displayLauncher ?
); } diff --git a/src/server/apis/google/GoogleApiServerUtils.ts b/src/server/apis/google/GoogleApiServerUtils.ts index 963c7736a..5714c9928 100644 --- a/src/server/apis/google/GoogleApiServerUtils.ts +++ b/src/server/apis/google/GoogleApiServerUtils.ts @@ -25,7 +25,8 @@ export namespace GoogleApiServerUtils { 'drive.file', 'photoslibrary', 'photoslibrary.appendonly', - 'photoslibrary.sharing' + 'photoslibrary.sharing', + 'userinfo.profile' ]; export const parseBuffer = (data: Buffer) => JSON.parse(data.toString()); @@ -96,21 +97,61 @@ export namespace GoogleApiServerUtils { }); }; - export const ProcessClientSideCode = async (information: CredentialInformation, authenticationCode: string): Promise => { + export interface GoogleAuthenticationResult { + access_token: string; + avatar: string; + name: string; + } + export const ProcessClientSideCode = async (information: CredentialInformation, authenticationCode: string): Promise => { const oAuth2Client = await RetrieveOAuthClient(information); - return new Promise((resolve, reject) => { + return new Promise((resolve, reject) => { oAuth2Client.getToken(authenticationCode, async (err, token) => { if (err || !token) { reject(err); return console.error('Error retrieving access token', err); } oAuth2Client.setCredentials(token); - await Database.Auxiliary.GoogleAuthenticationToken.Write(information.userId, token); - resolve({ token, client: oAuth2Client }); + const enriched = injectUserInfo(token); + await Database.Auxiliary.GoogleAuthenticationToken.Write(information.userId, enriched); + const { given_name, picture } = enriched.userInfo; + resolve({ + access_token: enriched.access_token!, + avatar: picture, + name: given_name + }); }); }); }; + /** + * It's pretty cool: the credentials id_token is split into thirds by periods. + * The middle third contains a base64-encoded JSON string with all the + * user info contained in the interface below. So, we isolate that middle third, + * base64 decode with atob and parse the JSON. + * @param credentials the client credentials returned from OAuth after the user + * has executed the authentication routine + */ + const injectUserInfo = (credentials: Credentials): EnrichedCredentials => { + const userInfo = JSON.parse(atob(credentials.id_token!.split(".")[1])); + return { ...credentials, userInfo }; + }; + + export type EnrichedCredentials = Credentials & { userInfo: UserInfo }; + export interface UserInfo { + at_hash: string; + aud: string; + azp: string; + exp: number; + family_name: string; + given_name: string; + iat: number; + iss: string; + locale: string; + name: string; + picture: string; + sub: string; + } + export const RetrieveCredentials = (information: CredentialInformation) => { return new Promise((resolve, reject) => { readFile(information.credentialsPath, async (err, credentials) => { diff --git a/src/server/database.ts b/src/server/database.ts index 990441d5a..db86b472d 100644 --- a/src/server/database.ts +++ b/src/server/database.ts @@ -4,6 +4,7 @@ import { Opt } from '../new_fields/Doc'; import { Utils, emptyFunction } from '../Utils'; import { DashUploadUtils } from './DashUploadUtils'; import { Credentials } from 'google-auth-library'; +import { GoogleApiServerUtils } from './apis/google/GoogleApiServerUtils'; export namespace Database { @@ -259,8 +260,8 @@ export namespace Database { return SanitizedSingletonQuery({ userId }, GoogleAuthentication, removeId); }; - export const Write = async (userId: string, token: any) => { - return Instance.insert({ userId, canAccess: [], ...token }, GoogleAuthentication); + export const Write = async (userId: string, enrichedCredentials: GoogleApiServerUtils.EnrichedCredentials) => { + return Instance.insert({ userId, canAccess: [], ...enrichedCredentials }, GoogleAuthentication); }; export const Update = async (userId: string, access_token: string, expiry_date: number) => { diff --git a/src/server/index.ts b/src/server/index.ts index 86c226a21..010a851bc 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -958,8 +958,7 @@ app.get(RouteStore.readGoogleAccessToken, async (req, res) => { app.post(RouteStore.writeGoogleAccessToken, async (req, res) => { const userId = req.header("userId")!; const information = { credentialsPath, userId }; - const { token } = await GoogleApiServerUtils.ProcessClientSideCode(information, req.body.authenticationCode); - res.send(token.access_token); + res.send(await GoogleApiServerUtils.ProcessClientSideCode(information, req.body.authenticationCode)); }); const tokenError = "Unable to successfully upload bytes for all images!"; -- cgit v1.2.3-70-g09d2