@@ -2,8 +2,10 @@ package oauth
22
33import (
44 "crypto/rand"
5+ "embed"
56 "encoding/base64"
67 "fmt"
8+ "html/template"
79 "io"
810 "net"
911 "net/http"
@@ -14,6 +16,26 @@ import (
1416 "time"
1517)
1618
19+ //go:embed templates/*.html
20+ var templateFS embed.FS
21+
22+ var (
23+ errorTemplate * template.Template
24+ successTemplate * template.Template
25+ )
26+
27+ func init () {
28+ var err error
29+ errorTemplate , err = template .ParseFS (templateFS , "templates/error.html" )
30+ if err != nil {
31+ panic (fmt .Sprintf ("failed to parse error template: %v" , err ))
32+ }
33+ successTemplate , err = template .ParseFS (templateFS , "templates/success.html" )
34+ if err != nil {
35+ panic (fmt .Sprintf ("failed to parse success template: %v" , err ))
36+ }
37+ }
38+
1739const (
1840 // DefaultAuthTimeout is the default timeout for the OAuth authorization flow
1941 DefaultAuthTimeout = 5 * time .Minute
@@ -95,70 +117,10 @@ func createCallbackHandler(expectedState string, codeChan chan<- string, errChan
95117 errChan <- fmt .Errorf ("authorization failed: %s" , errMsg )
96118
97119 w .Header ().Set ("Content-Type" , "text/html; charset=utf-8" )
98- fmt .Fprintf (w , `<!DOCTYPE html>
99- <html>
100- <head>
101- <meta charset="utf-8">
102- <title>Authorization Failed</title>
103- <style>
104- body {
105- font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Noto Sans", Helvetica, Arial, sans-serif;
106- background: linear-gradient(135deg, #0d1117 0%%, #161b22 100%%);
107- color: #e6edf3;
108- min-height: 100vh;
109- margin: 0;
110- display: flex;
111- align-items: center;
112- justify-content: center;
113- }
114- .container {
115- text-align: center;
116- padding: 48px;
117- background: #21262d;
118- border-radius: 12px;
119- border: 1px solid #30363d;
120- box-shadow: 0 8px 24px rgba(0,0,0,0.4);
121- max-width: 400px;
122- }
123- .icon {
124- width: 64px;
125- height: 64px;
126- margin-bottom: 24px;
127- }
128- h1 {
129- color: #f85149;
130- font-size: 24px;
131- font-weight: 600;
132- margin: 0 0 16px 0;
133- }
134- p {
135- color: #8b949e;
136- margin: 8px 0;
137- line-height: 1.5;
138- }
139- .error {
140- background: #21262d;
141- border: 1px solid #f8514966;
142- border-radius: 6px;
143- padding: 12px;
144- margin-top: 16px;
145- font-family: monospace;
146- font-size: 13px;
147- color: #f85149;
148- }
149- </style>
150- </head>
151- <body>
152- <div class="container">
153- <svg class="icon" viewBox="0 0 24 24" fill="#e6edf3">
154- <path d="M12 0C5.37 0 0 5.37 0 12c0 5.31 3.435 9.795 8.205 11.385.6.105.825-.255.825-.57 0-.285-.015-1.23-.015-2.235-3.015.555-3.795-.735-4.035-1.41-.135-.345-.72-1.41-1.23-1.695-.42-.225-1.02-.78-.015-.795.945-.015 1.62.87 1.845 1.23 1.08 1.815 2.805 1.305 3.495.99.105-.78.42-1.305.765-1.605-2.67-.3-5.46-1.335-5.46-5.925 0-1.305.465-2.385 1.23-3.225-.12-.3-.54-1.53.12-3.18 0 0 1.005-.315 3.3 1.23.96-.27 1.98-.405 3-.405s2.04.135 3 .405c2.295-1.56 3.3-1.23 3.3-1.23.66 1.65.24 2.88.12 3.18.765.84 1.23 1.905 1.23 3.225 0 4.605-2.805 5.625-5.475 5.925.435.375.81 1.095.81 2.22 0 1.605-.015 2.895-.015 3.3 0 .315.225.69.825.57A12.02 12.02 0 0024 12c0-6.63-5.37-12-12-12z"/>
155- </svg>
156- <h1>Authorization Failed</h1>
157- <p class="error">%s</p>
158- <p>You can close this window.</p>
159- </div>
160- </body>
161- </html>` , errMsg )
120+ // html/template auto-escapes ErrorMessage to prevent XSS
121+ if err := errorTemplate .Execute (w , struct { ErrorMessage string }{ErrorMessage : errMsg }); err != nil {
122+ http .Error (w , "Internal error" , http .StatusInternalServerError )
123+ }
162124 return
163125 }
164126
182144
183145 // Display success page
184146 w .Header ().Set ("Content-Type" , "text/html; charset=utf-8" )
185- fmt .Fprint (w , `<!DOCTYPE html>
186- <html>
187- <head>
188- <meta charset="utf-8">
189- <title>Authorization Successful</title>
190- <style>
191- body {
192- font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Noto Sans", Helvetica, Arial, sans-serif;
193- background: linear-gradient(135deg, #0d1117 0%, #161b22 100%);
194- color: #e6edf3;
195- min-height: 100vh;
196- margin: 0;
197- display: flex;
198- align-items: center;
199- justify-content: center;
200- }
201- .container {
202- text-align: center;
203- padding: 48px;
204- background: #21262d;
205- border-radius: 12px;
206- border: 1px solid #30363d;
207- box-shadow: 0 8px 24px rgba(0,0,0,0.4);
208- max-width: 400px;
209- }
210- .icon {
211- width: 64px;
212- height: 64px;
213- margin-bottom: 24px;
214- }
215- h1 {
216- color: #3fb950;
217- font-size: 24px;
218- font-weight: 600;
219- margin: 0 0 16px 0;
220- }
221- p {
222- color: #8b949e;
223- margin: 8px 0;
224- line-height: 1.5;
225- }
226- .hint {
227- margin-top: 24px;
228- padding: 12px;
229- background: #161b22;
230- border-radius: 6px;
231- font-size: 14px;
232- }
233- </style>
234- </head>
235- <body>
236- <div class="container">
237- <svg class="icon" viewBox="0 0 24 24" fill="#e6edf3">
238- <path d="M12 0C5.37 0 0 5.37 0 12c0 5.31 3.435 9.795 8.205 11.385.6.105.825-.255.825-.57 0-.285-.015-1.23-.015-2.235-3.015.555-3.795-.735-4.035-1.41-.135-.345-.72-1.41-1.23-1.695-.42-.225-1.02-.78-.015-.795.945-.015 1.62.87 1.845 1.23 1.08 1.815 2.805 1.305 3.495.99.105-.78.42-1.305.765-1.605-2.67-.3-5.46-1.335-5.46-5.925 0-1.305.465-2.385 1.23-3.225-.12-.3-.54-1.53.12-3.18 0 0 1.005-.315 3.3 1.23.96-.27 1.98-.405 3-.405s2.04.135 3 .405c2.295-1.56 3.3-1.23 3.3-1.23.66 1.65.24 2.88.12 3.18.765.84 1.23 1.905 1.23 3.225 0 4.605-2.805 5.625-5.475 5.925.435.375.81 1.095.81 2.22 0 1.605-.015 2.895-.015 3.3 0 .315.225.69.825.57A12.02 12.02 0 0024 12c0-6.63-5.37-12-12-12z"/>
239- </svg>
240- <h1>Authorization Successful</h1>
241- <p>You have successfully authorized the GitHub MCP Server.</p>
242- <div class="hint">
243- <p>You can close this window and retry your request.</p>
244- </div>
245- </div>
246- </body>
247- </html>` )
147+ if err := successTemplate .Execute (w , nil ); err != nil {
148+ http .Error (w , "Internal error" , http .StatusInternalServerError )
149+ }
248150 })
249151
250152 return mux
0 commit comments