Skip to content

Commit 1d7d8f8

Browse files
authored
feat: beautify auth page after pkce login (aws#4738)
* feat: beautify auth page after pkce login Problem: - Old screen was ugly and basic Solution: - New screen is similiar to the current oidc login webpage NOTE: We need to manually copy the html to dist because the vue loader doesn't do it for us. Also, it's a lot simpler to just serve the html rather than do server side loading for the vue component
1 parent 70e5525 commit 1d7d8f8

File tree

7 files changed

+274
-31
lines changed

7 files changed

+274
-31
lines changed
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
@import url('./icons.css');
2+
3+
html {
4+
height: 100%;
5+
}
6+
7+
body {
8+
box-sizing: border-box;
9+
min-height: 100%;
10+
margin: 0;
11+
padding: 15px 30px;
12+
display: flex;
13+
flex-direction: column;
14+
font-family: Verdana, Geneva, Tahoma, sans-serif;
15+
font-size: 0.9rem;
16+
background-color: #f2f3f3;
17+
justify-content: center;
18+
min-width: 400px;
19+
}
20+
21+
.flex-container {
22+
background-color: #ffffff;
23+
width: 30%;
24+
margin: 0 auto;
25+
padding: 2% 2% 1% 2%;
26+
max-width: 400px;
27+
box-shadow: 0px 1px 1px 1px #8a969a;
28+
}
29+
30+
.request {
31+
border-block: 1px solid;
32+
border-inline: 1px solid;
33+
border-end-end-radius: 3px;
34+
border-end-start-radius: 3px;
35+
border-start-end-radius: 3px;
36+
border-start-start-radius: 3px;
37+
margin-top: 5%;
38+
padding: 5%;
39+
display: flex;
40+
flex-direction: row;
41+
}
42+
43+
.request h4 {
44+
margin: 1% 1% 1% 0%;
45+
}
46+
47+
.request p {
48+
margin-bottom: 0;
49+
margin-top: 2%;
50+
}
51+
52+
.approval {
53+
background-color: #f2f8f0;
54+
border-color: #1d8102;
55+
}
56+
57+
.denial {
58+
background-color: #fff7f7;
59+
border-color: #d91515;
60+
}
61+
62+
.request-container {
63+
width: 100%;
64+
}
65+
66+
.success-icon {
67+
color: #1d8102;
68+
padding-right: 5px;
69+
}
70+
71+
.denial-icon {
72+
color: #d91515;
73+
padding-right: 5px;
74+
}
75+
76+
.center-icon {
77+
width: 100%;
78+
text-align: center;
79+
}
80+
81+
.hidden {
82+
display: none;
83+
visibility: none;
84+
}
85+
86+
.hint {
87+
color: #545b64;
88+
}
89+
90+
.aws-icon {
91+
margin-bottom: 50px;
92+
}

packages/core/scripts/build/copyFiles.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ const tasks: CopyTask[] = [
2929
{ target: path.join('src', 'test', 'shared', 'cloudformation', 'yaml') },
3030
{ target: path.join('src', 'test', 'codewhisperer', 'service', 'resources') },
3131
{ target: path.join('src', 'testFixtures') },
32+
{ target: 'src/auth/sso/vue' },
3233

3334
// SSM
3435
{

packages/core/src/auth/sso/server.ts

Lines changed: 65 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,14 @@
33
* SPDX-License-Identifier: Apache-2.0
44
*/
55

6+
import * as path from 'path'
67
import http from 'http'
78
import { getLogger } from '../../shared/logger'
89
import { ToolkitError } from '../../shared/errors'
910
import { Socket } from 'net'
1011
import globals from '../../shared/extensionGlobals'
1112
import { Result } from '../../shared/utilities/result'
13+
import { FileSystemCommon } from '../../srcShared/fs'
1214

1315
export class MissingPortError extends ToolkitError {
1416
constructor() {
@@ -45,8 +47,8 @@ export class AuthError extends ToolkitError {
4547
* back to VSCode
4648
*/
4749
export class AuthSSOServer {
48-
public baseUrl = `http://127.0.0.1`
49-
private oauthCallback = '/'
50+
private baseUrl = `http://127.0.0.1`
51+
private oauthCallback = '/oauth/callback'
5052
private authenticationFlowTimeoutInMs = 600000
5153
private authenticationWarningTimeoutInMs = 60000
5254

@@ -55,14 +57,18 @@ export class AuthSSOServer {
5557
private server: http.Server
5658
private connections: Socket[]
5759

58-
constructor(private readonly state: string, private readonly vscodeUriPath: string) {
60+
constructor(
61+
private readonly state: string,
62+
private readonly vscodeUriPath: string,
63+
private readonly scopes: string[]
64+
) {
5965
this.authenticationPromise = new Promise<Result<string>>(resolve => {
6066
this.deferred = { resolve }
6167
})
6268

6369
this.connections = []
6470

65-
this.server = http.createServer((req, res) => {
71+
this.server = http.createServer(async (req, res) => {
6672
res.setHeader('Access-Control-Allow-Methods', 'GET')
6773

6874
if (!req.url) {
@@ -76,7 +82,18 @@ export class AuthSSOServer {
7682
break
7783
}
7884
default: {
79-
getLogger().info('AuthSSOServer: missing redirection path name')
85+
if (url.pathname.startsWith('/resources')) {
86+
const iconPath = path.join(globals.context.extensionUri.fsPath, url.pathname)
87+
await this.loadResource(res, iconPath)
88+
break
89+
}
90+
const resourcePath = path.join(
91+
globals.context.extensionUri.fsPath,
92+
'dist/src/auth/sso/vue',
93+
url.pathname
94+
)
95+
await this.loadResource(res, resourcePath)
96+
break
8097
}
8198
}
8299
})
@@ -132,6 +149,10 @@ export class AuthSSOServer {
132149
}
133150

134151
public get redirectUri(): string {
152+
return `${this.baseLocation}${this.oauthCallback}`
153+
}
154+
155+
private get baseLocation(): string {
135156
return `${this.baseUrl}:${this.getPort()}`
136157
}
137158

@@ -150,6 +171,36 @@ export class AuthSSOServer {
150171
}
151172
}
152173

174+
private redirect(
175+
res: http.ServerResponse,
176+
params:
177+
| {
178+
productName: string
179+
scopes: string
180+
redirectUri: string
181+
}
182+
| {
183+
error: string
184+
}
185+
) {
186+
const redirectUrl = `${this.baseLocation}/index.html?${new URLSearchParams(params).toString()}`
187+
res.setHeader('Location', redirectUrl)
188+
res.writeHead(302)
189+
res.end()
190+
}
191+
192+
private async loadResource(res: http.ServerResponse, resourcePath: string) {
193+
try {
194+
const file = await FileSystemCommon.instance.readFile(resourcePath)
195+
res.writeHead(200)
196+
res.end(file)
197+
} catch (e) {
198+
getLogger().error(`Unable to find ${resourcePath}`)
199+
res.writeHead(404)
200+
res.end()
201+
}
202+
}
203+
153204
private handleAuthentication(params: URLSearchParams, res: http.ServerResponse) {
154205
const error = params.get('error')
155206
const errorDescription = params.get('error_description')
@@ -176,23 +227,19 @@ export class AuthSSOServer {
176227
}
177228

178229
this.deferred?.resolve(Result.ok(code))
179-
res.setHeader('Content-Type', 'text/html')
180-
res.writeHead(200)
181-
res.end(`
182-
<html>
183-
<body>
184-
<p>Authenticated successfully. You may now close this window.</p>
185-
<script>
186-
window.location.replace('${this.vscodeUriPath}')
187-
</script>
188-
</body>
189-
</html>`)
230+
231+
this.redirect(res, {
232+
productName: 'AWS Toolkit for VSCode',
233+
scopes: this.scopes.join(', '),
234+
redirectUri: this.vscodeUriPath,
235+
})
190236
}
191237

192238
private handleRequestRejection(res: http.ServerResponse, error: ToolkitError) {
193239
// Notify the user
194-
res.writeHead(400)
195-
res.end(error.message)
240+
this.redirect(res, {
241+
error: error.message,
242+
})
196243

197244
// Send the response back to the editor
198245
this.deferred?.resolve(Result.err(error))

packages/core/src/auth/sso/ssoAccessTokenProvider.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -392,7 +392,7 @@ class AuthFlowAuthorization extends SsoAccessTokenProvider {
392392
clientType: clientRegistrationType,
393393
scopes: this.profile.scopes,
394394
grantTypes: [authorizationGrantType, refreshGrantType],
395-
redirectUris: ['http://127.0.0.1'],
395+
redirectUris: ['http://127.0.0.1/oauth/callback'],
396396
issuerUrl: this.profile.startUrl,
397397
})
398398
}
@@ -401,7 +401,11 @@ class AuthFlowAuthorization extends SsoAccessTokenProvider {
401401
registration: ClientRegistration
402402
): Promise<{ token: SsoToken; registration: ClientRegistration; region: string; startUrl: string }> {
403403
const state = randomUUID()
404-
const authServer = new AuthSSOServer(state, UriHandler.buildUri(authenticationPath).toString())
404+
const authServer = new AuthSSOServer(
405+
state,
406+
UriHandler.buildUri(authenticationPath).toString(),
407+
this.profile.scopes ?? []
408+
)
405409

406410
try {
407411
await authServer.start()
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="utf-8" />
5+
<title>AWS Authentication</title>
6+
<meta name="viewport" content="width=device-width, initial-scale=1" />
7+
<link rel="stylesheet" type="text/css" media="screen" href="../../../../resources/css/auth.css" />
8+
</head>
9+
10+
<body>
11+
<div class="center-wrapper">
12+
<div style="font-size: 3rem" class="center-icon aws-icon">
13+
<svg
14+
id="Layer_1"
15+
data-name="Layer 1"
16+
xmlns="http://www.w3.org/2000/svg"
17+
width="6rem"
18+
:height="`${6 * 0.61}rem`"
19+
viewBox="0 0 50 30"
20+
>
21+
<path
22+
id="logo-text"
23+
d="M14.09,10.85a4.7,4.7,0,0,0,.19,1.48,7.73,7.73,0,0,0,.54,1.19.77.77,0,0,1,.12.38.64.64,0,0,1-.32.49l-1,.7a.83.83,0,0,1-.44.15.69.69,0,0,1-.49-.23,3.8,3.8,0,0,1-.6-.77q-.25-.42-.51-1a6.14,6.14,0,0,1-4.89,2.3,4.54,4.54,0,0,1-3.32-1.19,4.27,4.27,0,0,1-1.22-3.2A4.28,4.28,0,0,1,3.61,7.75,6.06,6.06,0,0,1,7.69,6.46a12.47,12.47,0,0,1,1.76.13q.92.13,1.91.36V5.73a3.65,3.65,0,0,0-.79-2.66A3.81,3.81,0,0,0,7.86,2.3a7.71,7.71,0,0,0-1.79.22,12.78,12.78,0,0,0-1.79.57,4.55,4.55,0,0,1-.58.22l-.26,0q-.35,0-.35-.52V2a1.09,1.09,0,0,1,.12-.58,1.2,1.2,0,0,1,.47-.35A10.88,10.88,0,0,1,5.77.32,10.19,10.19,0,0,1,8.36,0a6,6,0,0,1,4.35,1.35,5.49,5.49,0,0,1,1.38,4.09ZM7.34,13.38a5.36,5.36,0,0,0,1.72-.31A3.63,3.63,0,0,0,10.63,12,2.62,2.62,0,0,0,11.19,11a5.63,5.63,0,0,0,.16-1.44v-.7a14.35,14.35,0,0,0-1.53-.28,12.37,12.37,0,0,0-1.56-.1,3.84,3.84,0,0,0-2.47.67A2.34,2.34,0,0,0,5,11a2.35,2.35,0,0,0,.61,1.76A2.4,2.4,0,0,0,7.34,13.38Zm13.35,1.8a1,1,0,0,1-.64-.16,1.3,1.3,0,0,1-.35-.65L15.81,1.51a3,3,0,0,1-.15-.67.36.36,0,0,1,.41-.41H17.7a1,1,0,0,1,.65.16,1.4,1.4,0,0,1,.33.65l2.79,11,2.59-11A1.17,1.17,0,0,1,24.39.6a1.1,1.1,0,0,1,.67-.16H26.4a1.1,1.1,0,0,1,.67.16,1.17,1.17,0,0,1,.32.65L30,12.39,32.88,1.25A1.39,1.39,0,0,1,33.22.6a1,1,0,0,1,.65-.16h1.54a.36.36,0,0,1,.41.41,1.36,1.36,0,0,1,0,.26,3.64,3.64,0,0,1-.12.41l-4,12.86a1.3,1.3,0,0,1-.35.65,1,1,0,0,1-.64.16H29.25a1,1,0,0,1-.67-.17,1.26,1.26,0,0,1-.32-.67L25.67,3.64,23.11,14.34a1.26,1.26,0,0,1-.32.67,1,1,0,0,1-.67.17Zm21.36.44a11.28,11.28,0,0,1-2.56-.29,7.44,7.44,0,0,1-1.92-.67,1,1,0,0,1-.61-.93v-.84q0-.52.38-.52a.9.9,0,0,1,.31.06l.42.17a8.77,8.77,0,0,0,1.83.58,9.78,9.78,0,0,0,2,.2,4.48,4.48,0,0,0,2.43-.55,1.76,1.76,0,0,0,.86-1.57,1.61,1.61,0,0,0-.45-1.16A4.29,4.29,0,0,0,43,9.22l-2.41-.76A5.15,5.15,0,0,1,38,6.78a3.94,3.94,0,0,1-.83-2.41,3.7,3.7,0,0,1,.45-1.85,4.47,4.47,0,0,1,1.19-1.37A5.27,5.27,0,0,1,40.51.29,7.4,7.4,0,0,1,42.6,0a8.87,8.87,0,0,1,1.12.07q.57.07,1.08.19t.95.26a4.27,4.27,0,0,1,.7.29,1.59,1.59,0,0,1,.49.41.94.94,0,0,1,.15.55v.79q0,.52-.38.52a1.76,1.76,0,0,1-.64-.2,7.74,7.74,0,0,0-3.2-.64,4.37,4.37,0,0,0-2.21.47,1.6,1.6,0,0,0-.79,1.48,1.58,1.58,0,0,0,.49,1.18,4.94,4.94,0,0,0,1.83.92L44.55,7a5.08,5.08,0,0,1,2.57,1.6A3.76,3.76,0,0,1,47.9,11a4.21,4.21,0,0,1-.44,1.93,4.4,4.4,0,0,1-1.21,1.47,5.43,5.43,0,0,1-1.85.93A8.25,8.25,0,0,1,42.05,15.62Z"
24+
/>
25+
<path
26+
fill="#FF9900"
27+
class="cls-1"
28+
d="M45.19,23.81C39.72,27.85,31.78,30,25,30A36.64,36.64,0,0,1,.22,20.57c-.51-.46-.06-1.09.56-.74A49.78,49.78,0,0,0,25.53,26.4,49.23,49.23,0,0,0,44.4,22.53C45.32,22.14,46.1,23.14,45.19,23.81Z"
29+
/>
30+
<path
31+
fill="#FF9900"
32+
class="cls-1"
33+
d="M47.47,21.21c-.7-.9-4.63-.42-6.39-.21-.53.06-.62-.4-.14-.74,3.13-2.2,8.27-1.57,8.86-.83s-.16,5.89-3.09,8.35c-.45.38-.88.18-.68-.32C46.69,25.8,48.17,22.11,47.47,21.21Z"
34+
/>
35+
</svg>
36+
</div>
37+
<div class="flex-container">
38+
<!-- Section for request approval -->
39+
<div id="approved-auth">
40+
<div class="request approval">
41+
<span class="pass-icon icon-2x icon icon-vscode-pass success-icon"></span>
42+
<div class="request-container">
43+
<h4>Request approved</h4>
44+
<p id="approvalMessage" class="hint"></p>
45+
</div>
46+
</div>
47+
<p id="footerText" class="hint"></p>
48+
</div>
49+
50+
<!-- Section for request denial -->
51+
<div id="denied-auth" class="hidden">
52+
<div class="request denial">
53+
<span class="icon-2x icon icon-vscode-error denial-icon"></span>
54+
<div class="request-container">
55+
<h4>Request denied</h4>
56+
<p id="errorMessage" class="hint"></p>
57+
</div>
58+
</div>
59+
<p class="hint">You can close this window and re-start the authorization flow</p>
60+
</div>
61+
</div>
62+
</div>
63+
<script>
64+
window.onload = () => {
65+
const params = new URLSearchParams(window.location.search)
66+
67+
const error = params.get('error')
68+
if (error) {
69+
showErrorMessage(error)
70+
return
71+
}
72+
73+
const productName = params.get('productName')
74+
const scopes = params.get('scopes')
75+
if (!productName || !scopes) {
76+
showErrorMessage('Unable to find productName and scopes')
77+
return
78+
}
79+
80+
document.getElementById(
81+
'approvalMessage'
82+
).innerText = `${productName} can now access your data in ${scopes}.`
83+
document.getElementById(
84+
'footerText'
85+
).innerText = `You can close this window and start using ${productName}`
86+
87+
const redirectUri = params.get('redirectUri')
88+
if (redirectUri) {
89+
window.location.replace(redirectUri)
90+
}
91+
92+
function showErrorMessage(errorText) {
93+
document.getElementById('approved-auth').classList.add('hidden')
94+
document.getElementById('denied-auth').classList.remove('hidden')
95+
document.getElementById('errorMessage').innerText = errorText
96+
}
97+
}
98+
</script>
99+
</body>
100+
</html>

0 commit comments

Comments
 (0)