Skip to content

Commit af738e6

Browse files
committed
Add password authentication support
- Add password authentication option to AuthRouter - Support SHA256 password hashing with multiple passwords - Update login template with password form and improved styling - Add password error handling and session management - Integrate password auth with existing OAuth providers
1 parent 9c6e916 commit af738e6

3 files changed

Lines changed: 187 additions & 23 deletions

File tree

pkg/auth/main.go

Lines changed: 77 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,12 @@ package auth
22

33
import (
44
"context"
5+
"crypto/sha256"
56
"embed"
7+
"fmt"
68
"html/template"
79
"net/http"
10+
"strings"
811

912
"github.com/gin-contrib/sessions"
1013
"github.com/gin-gonic/gin"
@@ -25,12 +28,13 @@ type Provider interface {
2528
}
2629

2730
type AuthRouter struct {
28-
providers map[string]Provider
29-
template *template.Template
31+
passswordHash []string
32+
providers map[string]Provider
33+
template *template.Template
3034
unauthorizedTemplate *template.Template
3135
}
3236

33-
func NewAuthRouter(providers ...Provider) (*AuthRouter, error) {
37+
func NewAuthRouter(passwordHash []string, providers ...Provider) (*AuthRouter, error) {
3438
providersMap := make(map[string]Provider)
3539
for _, provider := range providers {
3640
providersMap[provider.Name()] = provider
@@ -40,22 +44,24 @@ func NewAuthRouter(providers ...Provider) (*AuthRouter, error) {
4044
if err != nil {
4145
return nil, err
4246
}
43-
47+
4448
unauthorizedTmpl, err := template.ParseFS(templateFS, "templates/unauthorized.html")
4549
if err != nil {
4650
return nil, err
4751
}
4852

4953
return &AuthRouter{
50-
providers: providersMap,
51-
template: tmpl,
54+
passswordHash: passwordHash,
55+
providers: providersMap,
56+
template: tmpl,
5257
unauthorizedTemplate: unauthorizedTmpl,
5358
}, nil
5459
}
5560

5661
const (
5762
LoginEndpoint = "/.auth/login"
5863
LogoutEndpoint = "/.auth/logout"
64+
PasswordEndpoint = "/.auth/password"
5965
GoogleAuthEndpoint = "/.auth/google"
6066
GoogleCallbackEndpoint = "/.auth/google/callback"
6167
GitHubAuthEndpoint = "/.auth/github"
@@ -64,6 +70,7 @@ const (
6470

6571
func (a *AuthRouter) SetupRoutes(router gin.IRouter) {
6672
router.GET(LoginEndpoint, a.handleLogin)
73+
router.POST(PasswordEndpoint, a.handlePasswordAuth)
6774
router.GET(LogoutEndpoint, a.handleLogout)
6875
for providerName, provider := range a.providers {
6976
router.GET(provider.RedirectURL(), func(c *gin.Context) {
@@ -111,10 +118,24 @@ func (a *AuthRouter) handleLogin(c *gin.Context) {
111118
})
112119
}
113120

121+
session := sessions.Default(c)
122+
passwordError := session.Get("password_error")
123+
session.Delete("password_error")
124+
session.Save()
125+
126+
var passwordErrorStr string
127+
if passwordError != nil {
128+
passwordErrorStr = passwordError.(string)
129+
}
130+
114131
data := struct {
115-
Providers []ProviderData
132+
Providers []ProviderData
133+
HasPassword bool
134+
PasswordError string
116135
}{
117-
Providers: providersData,
136+
Providers: providersData,
137+
HasPassword: len(a.passswordHash) > 0,
138+
PasswordError: passwordErrorStr,
118139
}
119140

120141
c.Header("Content-Type", "text/html; charset=utf-8")
@@ -124,6 +145,47 @@ func (a *AuthRouter) handleLogin(c *gin.Context) {
124145
}
125146
}
126147

148+
func (a *AuthRouter) handlePasswordAuth(c *gin.Context) {
149+
password := c.PostForm("password")
150+
if password == "" {
151+
session := sessions.Default(c)
152+
session.Set("password_error", "Password is required")
153+
session.Save()
154+
c.Redirect(http.StatusFound, LoginEndpoint)
155+
return
156+
}
157+
158+
hashedPassword := fmt.Sprintf("%x", sha256.Sum256([]byte(password)))
159+
160+
var isValid bool
161+
for _, hash := range a.passswordHash {
162+
if strings.EqualFold(hashedPassword, hash) {
163+
isValid = true
164+
break
165+
}
166+
}
167+
168+
if !isValid {
169+
session := sessions.Default(c)
170+
session.Set("password_error", "Invalid password")
171+
session.Save()
172+
c.Redirect(http.StatusFound, LoginEndpoint)
173+
return
174+
}
175+
176+
session := sessions.Default(c)
177+
session.Set("provider", "password")
178+
session.Set("user_id", "password_user")
179+
session.Save()
180+
181+
redirectURL := session.Get("redirect_url")
182+
if redirectURL == nil {
183+
c.Redirect(http.StatusFound, "/")
184+
return
185+
}
186+
c.Redirect(http.StatusFound, redirectURL.(string))
187+
}
188+
127189
func (a *AuthRouter) handleLogout(c *gin.Context) {
128190
session := sessions.Default(c)
129191
session.Clear()
@@ -142,6 +204,13 @@ func (a *AuthRouter) RequireAuth() gin.HandlerFunc {
142204
c.Redirect(http.StatusFound, LoginEndpoint)
143205
return
144206
}
207+
208+
// Allow password authentication
209+
if providerName.(string) == "password" {
210+
c.Next()
211+
return
212+
}
213+
145214
p, ok := a.providers[providerName.(string)]
146215
if !ok {
147216
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "Unknown provider"})

pkg/auth/templates/login.html

Lines changed: 109 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
<!DOCTYPE html>
2-
<html lang="ja">
2+
<html lang="en">
33
<head>
44
<meta charset="UTF-8" />
55
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
@@ -17,23 +17,96 @@
1717
}
1818
.login-container {
1919
background: white;
20-
padding: 2rem;
21-
border-radius: 10px;
22-
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1);
20+
padding: 2.5rem;
21+
border-radius: 12px;
22+
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.12);
2323
text-align: center;
24-
min-width: 300px;
24+
min-width: 350px;
25+
max-width: 400px;
26+
width: 100%;
2527
}
2628
h1 {
2729
color: #333;
30+
margin-bottom: 2rem;
31+
font-size: 2rem;
32+
font-weight: 600;
33+
}
34+
.password-form {
35+
margin-bottom: 2rem;
36+
padding-bottom: 2rem;
37+
border-bottom: 1px solid #e5e5e5;
38+
}
39+
.form-group {
2840
margin-bottom: 1.5rem;
41+
text-align: left;
42+
}
43+
.form-label {
44+
display: block;
45+
margin-bottom: 0.5rem;
46+
color: #555;
47+
font-weight: 500;
48+
font-size: 0.9rem;
49+
}
50+
.form-input {
51+
width: 100%;
52+
padding: 12px 16px;
53+
border: 2px solid #e1e5e9;
54+
border-radius: 8px;
55+
font-size: 16px;
56+
transition: border-color 0.2s, box-shadow 0.2s;
57+
box-sizing: border-box;
58+
}
59+
.form-input:focus {
60+
outline: none;
61+
border-color: #667eea;
62+
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
63+
}
64+
.password-button {
65+
width: 100%;
66+
padding: 12px 20px;
67+
background: linear-gradient(45deg, #667eea, #764ba2);
68+
color: white;
69+
border: none;
70+
border-radius: 8px;
71+
font-size: 16px;
72+
font-weight: 600;
73+
cursor: pointer;
74+
transition: transform 0.2s, box-shadow 0.2s;
75+
box-sizing: border-box;
76+
}
77+
.password-button:hover {
78+
transform: translateY(-2px);
79+
box-shadow: 0 6px 20px rgba(102, 126, 234, 0.3);
80+
}
81+
.divider {
82+
margin: 1.5rem 0;
83+
position: relative;
84+
color: #999;
85+
font-size: 0.9rem;
86+
}
87+
.divider::before {
88+
content: '';
89+
position: absolute;
90+
top: 50%;
91+
left: 0;
92+
right: 0;
93+
height: 1px;
94+
background: #e5e5e5;
95+
z-index: 1;
96+
}
97+
.divider span {
98+
background: white;
99+
padding: 0 1rem;
100+
position: relative;
101+
z-index: 2;
29102
}
30103
.provider-button {
31104
display: block;
32105
width: 100%;
33106
padding: 12px 20px;
34-
margin: 10px auto;
107+
margin: 12px auto;
35108
text-decoration: none;
36-
border-radius: 6px;
109+
border-radius: 8px;
37110
font-size: 16px;
38111
font-weight: 500;
39112
transition: transform 0.2s, box-shadow 0.2s;
@@ -47,7 +120,7 @@
47120
}
48121
.provider-button:hover {
49122
transform: translateY(-2px);
50-
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
123+
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.15);
51124
}
52125
.google {
53126
background-color: #4285f4;
@@ -63,19 +136,41 @@
63136
.github:hover {
64137
background-color: #24292e;
65138
}
66-
.description {
67-
color: #666;
68-
margin-bottom: 2rem;
69-
line-height: 1.5;
139+
.error-message {
140+
color: #e74c3c;
141+
font-size: 0.9rem;
142+
margin-top: 0.5rem;
143+
text-align: center;
70144
}
71145
</style>
72146
</head>
73147
<body>
74148
<div class="login-container">
75149
<h1>MCP Auth Proxy</h1>
76-
150+
151+
{{if .HasPassword}}
152+
<form class="password-form" action="/.auth/password" method="POST">
153+
<div class="form-group">
154+
<label class="form-label" for="password">Password</label>
155+
<input class="form-input" type="password" id="password" name="password" required>
156+
</div>
157+
<button class="password-button" type="submit">Login</button>
158+
{{if .PasswordError}}
159+
<div class="error-message">{{.PasswordError}}</div>
160+
{{end}}
161+
</form>
162+
{{end}}
163+
164+
{{if and .HasPassword .Providers}}
165+
<div class="divider">
166+
<span>or</span>
167+
</div>
168+
{{end}}
169+
77170
{{range .Providers}}
78-
<a href="{{.URL}}" class="provider-button"> Login with {{.Name}} </a>
171+
<a href="{{.URL}}" class="provider-button {{.Name | lower}}">
172+
Login with {{.Name}}
173+
</a>
79174
{{end}}
80175
</div>
81176
</body>

pkg/mcp-proxy/main.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ func Run(
8989
providers = append(providers, githubProvider)
9090
}
9191

92-
authRouter, err := auth.NewAuthRouter(providers...)
92+
authRouter, err := auth.NewAuthRouter([]string{}, providers...)
9393
if err != nil {
9494
return fmt.Errorf("failed to create auth router: %w", err)
9595
}

0 commit comments

Comments
 (0)