Skip to content

Commit 617523e

Browse files
authored
Make PKCE success/error page pretty (#4342)
1 parent 34d8079 commit 617523e

File tree

6 files changed

+253
-3
lines changed

6 files changed

+253
-3
lines changed

plugins/core/build.gradle.kts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,11 @@ dependencies {
3131
}
3232

3333
configurations {
34+
all {
35+
// IDE provides netty
36+
exclude("io.netty")
37+
}
38+
3439
// Make sure we exclude stuff we either A) ships with IDE, B) we don't use to cut down on size
3540
runtimeClasspath {
3641
exclude(group = "org.slf4j")

plugins/core/jetbrains-community/resources/META-INF/aws.toolkit.core.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212

1313
<extensions defaultExtensionNs="com.intellij">
1414
<httpRequestHandler implementation="software.aws.toolkits.jetbrains.core.credentials.sso.pkce.ToolkitOAuthCallbackHandler"/>
15+
<httpRequestHandler implementation="software.aws.toolkits.jetbrains.core.credentials.sso.pkce.ToolkitOAuthCallbackResultService"/>
1516

1617
<applicationService serviceInterface="software.aws.toolkits.jetbrains.settings.AwsSettings"
1718
serviceImplementation="software.aws.toolkits.jetbrains.settings.DefaultAwsSettings"
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
/* Copyright 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved. */
2+
/* SPDX-License-Identifier: Apache-2.0 */
3+
4+
html {
5+
height: 100%;
6+
}
7+
8+
body {
9+
box-sizing: border-box;
10+
min-height: 100%;
11+
margin: 0;
12+
padding: 15px 30px;
13+
display: flex;
14+
flex-direction: column;
15+
font-family: Verdana, Geneva, Tahoma, sans-serif;
16+
font-size: 0.9rem;
17+
background-color: #f2f3f3;
18+
justify-content: center;
19+
min-width: 400px;
20+
}
21+
22+
.flex-container {
23+
background-color: #ffffff;
24+
width: 30%;
25+
margin: 0 auto;
26+
padding: 2% 2% 1% 2%;
27+
max-width: 400px;
28+
box-shadow: 0px 1px 1px 1px #8a969a;
29+
}
30+
31+
.request {
32+
border-block: 1px solid;
33+
border-inline: 1px solid;
34+
border-end-end-radius: 3px;
35+
border-end-start-radius: 3px;
36+
border-start-end-radius: 3px;
37+
border-start-start-radius: 3px;
38+
margin-top: 5%;
39+
padding: 5%;
40+
display: flex;
41+
flex-direction: row;
42+
}
43+
44+
.request h4 {
45+
margin: 1% 1% 1% 0%;
46+
}
47+
48+
.request p {
49+
margin-bottom: 0;
50+
margin-top: 2%;
51+
}
52+
53+
.approval {
54+
background-color: #f2f8f0;
55+
border-color: #1d8102;
56+
}
57+
58+
.denial {
59+
background-color: #fff7f7;
60+
border-color: #d91515;
61+
}
62+
63+
.request-container {
64+
width: 100%;
65+
}
66+
67+
.success-icon {
68+
color: #1d8102;
69+
padding-right: 5px;
70+
}
71+
72+
.denial-icon {
73+
color: #d91515;
74+
padding-right: 5px;
75+
}
76+
77+
.center-icon {
78+
width: 100%;
79+
text-align: center;
80+
}
81+
82+
.hidden {
83+
display: none;
84+
visibility: none;
85+
}
86+
87+
.hint {
88+
color: #545b64;
89+
}
90+
91+
.aws-icon {
92+
margin-bottom: 50px;
93+
}
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
<!-- Copyright 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved. -->
2+
<!-- SPDX-License-Identifier: Apache-2.0 -->
3+
4+
<!DOCTYPE html>
5+
<html lang="en">
6+
<head>
7+
<meta charset="utf-8" />
8+
<title>AWS Authentication</title>
9+
<meta name="viewport" content="width=device-width, initial-scale=1" />
10+
<link rel="stylesheet" type="text/css" media="screen" href="auth.css" />
11+
</head>
12+
13+
<body>
14+
<div class="center-wrapper">
15+
<div style="font-size: 3rem" class="center-icon aws-icon">
16+
<svg
17+
id="Layer_1"
18+
data-name="Layer 1"
19+
xmlns="http://www.w3.org/2000/svg"
20+
width="6rem"
21+
:height="`${6 * 0.61}rem`"
22+
viewBox="0 0 50 30"
23+
>
24+
<path
25+
id="logo-text"
26+
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"
27+
/>
28+
<path
29+
fill="#FF9900"
30+
class="cls-1"
31+
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"
32+
/>
33+
<path
34+
fill="#FF9900"
35+
class="cls-1"
36+
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"
37+
/>
38+
</svg>
39+
</div>
40+
<div class="flex-container">
41+
<!-- Section for request approval -->
42+
<div id="approved-auth">
43+
<div class="request approval">
44+
<span class="pass-icon icon-2x icon icon-vscode-pass success-icon"></span>
45+
<div class="request-container">
46+
<h4>Request approved</h4>
47+
<p id="approvalMessage" class="hint"></p>
48+
</div>
49+
</div>
50+
<p id="footerText" class="hint"></p>
51+
</div>
52+
53+
<!-- Section for request denial -->
54+
<div id="denied-auth" class="hidden">
55+
<div class="request denial">
56+
<span class="icon-2x icon icon-vscode-error denial-icon"></span>
57+
<div class="request-container">
58+
<h4>Request denied</h4>
59+
<p id="errorMessage" class="hint"></p>
60+
</div>
61+
</div>
62+
<p class="hint">You can close this window and re-start the authorization flow</p>
63+
</div>
64+
</div>
65+
</div>
66+
<script>
67+
window.onload = () => {
68+
const params = new URLSearchParams(window.location.search)
69+
70+
const error = params.get('error')
71+
if (error) {
72+
showErrorMessage(error)
73+
return
74+
}
75+
76+
const productName = params.get('productName')
77+
const scopes = params.get('scopes')
78+
if (!productName || !scopes) {
79+
showErrorMessage('Unable to find productName and scopes')
80+
return
81+
}
82+
83+
document.getElementById(
84+
'approvalMessage'
85+
).innerText = `${productName} can now access your data in ${scopes}.`
86+
document.getElementById(
87+
'footerText'
88+
).innerText = `You can close this window and start using ${productName}`
89+
90+
const redirectUri = params.get('redirectUri')
91+
if (redirectUri) {
92+
window.location.replace(redirectUri)
93+
}
94+
95+
function showErrorMessage(errorText) {
96+
document.getElementById('approved-auth').classList.add('hidden')
97+
document.getElementById('denied-auth').classList.remove('hidden')
98+
document.getElementById('errorMessage').innerText = errorText
99+
}
100+
}
101+
</script>
102+
</body>
103+
</html>

plugins/core/jetbrains-community/src/software/aws/toolkits/jetbrains/core/credentials/sso/SsoAccessTokenProvider.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import software.amazon.awssdk.services.ssooidc.model.SlowDownException
1616
import software.aws.toolkits.core.utils.getLogger
1717
import software.aws.toolkits.core.utils.warn
1818
import software.aws.toolkits.jetbrains.core.credentials.sono.SONO_URL
19+
import software.aws.toolkits.jetbrains.core.credentials.sso.pkce.PKCE_CLIENT_NAME
1920
import software.aws.toolkits.jetbrains.core.credentials.sso.pkce.ToolkitOAuthService
2021
import software.aws.toolkits.jetbrains.utils.assertIsNonDispatchThread
2122
import software.aws.toolkits.jetbrains.utils.sleepWithCancellation
@@ -131,7 +132,7 @@ class SsoAccessTokenProvider(
131132
}
132133

133134
val registerResponse = client.registerClient {
134-
it.clientName("AWS IDE extensions for JetBrains")
135+
it.clientName(PKCE_CLIENT_NAME)
135136
it.clientType(PUBLIC_CLIENT_REGISTRATION_TYPE)
136137
it.scopes(scopes)
137138
it.grantTypes(PKCE_GRANT_TYPES)

plugins/core/jetbrains-community/src/software/aws/toolkits/jetbrains/core/credentials/sso/pkce/ToolkitOAuthService.kt

Lines changed: 49 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,27 +8,36 @@ import com.intellij.collaboration.auth.services.OAuthCredentialsAcquirer
88
import com.intellij.collaboration.auth.services.OAuthRequest
99
import com.intellij.collaboration.auth.services.OAuthServiceBase
1010
import com.intellij.collaboration.auth.services.PkceUtils
11+
import com.intellij.openapi.application.ApplicationNamesInfo
1112
import com.intellij.openapi.application.runInEdt
1213
import com.intellij.openapi.components.Service
1314
import com.intellij.openapi.components.service
1415
import com.intellij.openapi.wm.IdeFocusManager
1516
import com.intellij.util.Url
1617
import com.intellij.util.Urls.newFromEncoded
1718
import com.intellij.util.io.DigestUtil
19+
import io.netty.buffer.Unpooled
20+
import io.netty.channel.ChannelHandlerContext
1821
import io.netty.handler.codec.http.FullHttpRequest
22+
import io.netty.handler.codec.http.QueryStringDecoder
1923
import org.jetbrains.ide.BuiltInServerManager
24+
import org.jetbrains.ide.RestService
25+
import org.jetbrains.io.response
2026
import software.amazon.awssdk.regions.Region
2127
import software.amazon.awssdk.services.ssooidc.endpoints.SsoOidcEndpointParams
2228
import software.amazon.awssdk.services.ssooidc.endpoints.internal.DefaultSsoOidcEndpointProvider
2329
import software.aws.toolkits.jetbrains.core.credentials.sso.AccessToken
2430
import software.aws.toolkits.jetbrains.core.credentials.sso.PKCEAuthorizationGrantToken
2531
import software.aws.toolkits.jetbrains.core.credentials.sso.PKCEClientRegistration
2632
import software.aws.toolkits.jetbrains.core.credentials.sso.bearer.buildUnmanagedSsoOidcClient
33+
import software.aws.toolkits.resources.message
2734
import java.math.BigInteger
2835
import java.time.Instant
2936
import java.util.Base64
3037
import java.util.concurrent.CompletableFuture
3138

39+
const val PKCE_CLIENT_NAME = "AWS IDE Plugins for JetBrains"
40+
3241
@Service
3342
class ToolkitOAuthService : OAuthServiceBase<AccessToken>() {
3443
override val name: String = "aws/toolkit"
@@ -154,8 +163,23 @@ internal class ToolkitOAuthCallbackHandler : OAuthCallbackHandlerBase() {
154163
IdeFocusManager.getGlobalInstance().getLastFocusedIdeWindow()?.toFront()
155164
}
156165

157-
// provide a better page
158-
return AcceptCodeHandleResult.Page(if (isAccepted) "complete" else "error")
166+
val urlBase = newFromEncoded(
167+
"http://127.0.0.1:${BuiltInServerManager.getInstance().port}/api/${ToolkitOAuthCallbackResultService.SERVICE_NAME}/index.html"
168+
)
169+
val params = if (isAccepted) {
170+
mapOf(
171+
"productName" to PKCE_CLIENT_NAME,
172+
// we don't have the request context to get the requested scopes in this callback until 233
173+
"scopes" to ApplicationNamesInfo.getInstance().fullProductName
174+
)
175+
} else {
176+
mapOf(
177+
// when 233, check if we can retrieve the underlying error
178+
"error" to message("general.unknown_error")
179+
)
180+
}
181+
182+
return AcceptCodeHandleResult.Redirect(urlBase.addParameters(params))
159183
}
160184

161185
override fun isSupported(request: FullHttpRequest): Boolean {
@@ -168,3 +192,26 @@ internal class ToolkitOAuthCallbackHandler : OAuthCallbackHandlerBase() {
168192
return request.uri().trim('/').startsWith("oauth/callback")
169193
}
170194
}
195+
196+
internal class ToolkitOAuthCallbackResultService : RestService() {
197+
override fun execute(urlDecoder: QueryStringDecoder, request: FullHttpRequest, context: ChannelHandlerContext): String? {
198+
val path = urlDecoder.path().substringAfter(getServiceName()).trim('/')
199+
val type = when {
200+
path.endsWith(".css") -> "text/css"
201+
else -> "text/html"
202+
}
203+
val content = ToolkitOAuthCallbackResultService::class.java.getResourceAsStream("/oauthCallback/$path")?.readAllBytes() ?: return "Unknown resource"
204+
205+
val response = response(type, Unpooled.wrappedBuffer(content))
206+
sendResponse(request, context, response)
207+
208+
// return null on success
209+
return null
210+
}
211+
212+
override fun getServiceName() = SERVICE_NAME
213+
214+
companion object {
215+
const val SERVICE_NAME = "aws/toolkit/oauthResult"
216+
}
217+
}

0 commit comments

Comments
 (0)