+40
-43
html/pages/auth.templ
+40
-43
html/pages/auth.templ
···
1
1
package pages
2
2
3
-
import "strings"
3
+
import (
4
+
"strings"
5
+
"go.hacdias.com/indielib/indieauth"
6
+
)
4
7
5
-
import "go.hacdias.com/indielib/indieauth"
8
+
templ Auth( req *indieauth.AuthenticationRequest, app *indieauth.ApplicationMetadata, nonceId string, nonce string ) {
9
+
<h1>IndieAuth Server Demo: Authorization</h1>
6
10
7
-
templ Auth( req *indieauth.AuthenticationRequest, app *indieauth.ApplicationMetadata ) {
8
-
<!DOCTYPE html>
9
-
<html>
10
-
<head>
11
-
<title>Authorization | Micropub and IndieAuth Server Demo</title>
12
-
</head>
13
-
<body>
14
-
<h1>IndieAuth Server Demo: Authorization</h1>
11
+
<p>
12
+
You received an authorization request from
15
13
16
-
<p>
17
-
You received an authorization request from
14
+
if app != nil {
15
+
if len(app.Logo) > 0 {
16
+
<img style="width: 1em; vertical-align: middle" src={ app.Logo } />
17
+
}
18
18
19
-
if app != nil {
20
-
if len(app.Logo) > 0 {
21
-
<img style="width: 1em; vertical-align: middle" src={ app.Logo } />
22
-
}
19
+
<strong>{ app.Name }</strong> by { app.Author }:
20
+
} else {
21
+
the following client:
22
+
}
23
+
</p>
23
24
24
-
<strong>{ app.Name }</strong> by { app.Author }:
25
-
} else {
26
-
the following client:
27
-
}
28
-
</p>
25
+
<ul>
26
+
<li><strong>Redirect:</strong> <code>{ req.ClientID }</code></li>
27
+
<li><strong>Client:</strong> <code>{ req.RedirectURI }</code></li>
28
+
</ul>
29
29
30
-
<ul>
31
-
<li><strong>Redirect:</strong> <code>{ req.ClientID }</code></li>
32
-
<li><strong>Client:</strong> <code>{ req.RedirectURI }</code></li>
33
-
</ul>
30
+
<p>For the following scopes:
31
+
for _, scope := range req.Scopes {
32
+
<code>{ scope }</code>
33
+
}
34
+
.</p>
34
35
35
-
<p>For the following scopes:
36
-
for _, scope := range req.Scopes {
37
-
<code>{ scope }</code>
38
-
}
39
-
.</p>
36
+
<form method='post' action='/authorization/accept'>
37
+
<input type="hidden" name="response_type" value="code"/>
38
+
<input type="hidden" name="scope" value={ strings.Join(req.Scopes, " ") }/>
39
+
<input type="hidden" name="redirect_uri" value={ req.RedirectURI }/>
40
+
<input type="hidden" name="client_id" value={ req.ClientID }/>
41
+
<input type="hidden" name="state" value={ req.State }/>
42
+
<input type="hidden" name="code_challenge" value={ req.CodeChallenge }/>
43
+
<input type="hidden" name="code_challenge_method" value={ req.CodeChallengeMethod }/>
40
44
41
-
<form method='post' action='/authorization/accept'>
42
-
<input type="hidden" name="response_type" value="code">
43
-
<input type="hidden" name="scope" value={ strings.Join(req.Scopes, " ") }>
44
-
<input type="hidden" name="redirect_uri" value={ req.RedirectURI }>
45
-
<input type="hidden" name="client_id" value={ req.ClientID }>
46
-
<input type="hidden" name="state" value={ req.State }>
47
-
<input type="hidden" name="code_challenge" value={ req.CodeChallenge }>
48
-
<input type="hidden" name="code_challenge_method" value={ req.CodeChallengeMethod }>
45
+
// CSRF protections
46
+
<input type="hidden" name="nonce_id" value={ nonceId }/>
47
+
<input type="hidden" name="nonce" value={ nonce }/>
49
48
50
-
<p>In a production server, this page could be behind some sort of authentication mechanism, such as username and password, PassKey, etc.</p>
49
+
<p>In a production server, this page could be behind some sort of authentication mechanism, such as username and password, PassKey, etc.</p>
51
50
52
-
<button id="submit">Authorize</button>
53
-
</form>
54
-
</body>
55
-
</html>
51
+
<button id="submit">Authorize</button>
52
+
</form>
56
53
}
+45
-18
html/pages/auth_templ.go
+45
-18
html/pages/auth_templ.go
···
8
8
import "github.com/a-h/templ"
9
9
import templruntime "github.com/a-h/templ/runtime"
10
10
11
-
import "strings"
12
-
13
-
import "go.hacdias.com/indielib/indieauth"
11
+
import (
12
+
"go.hacdias.com/indielib/indieauth"
13
+
"strings"
14
+
)
14
15
15
-
func Auth(req *indieauth.AuthenticationRequest, app *indieauth.ApplicationMetadata) templ.Component {
16
+
func Auth(req *indieauth.AuthenticationRequest, app *indieauth.ApplicationMetadata, nonceId string, nonce string) templ.Component {
16
17
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
17
18
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
18
19
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
···
30
31
templ_7745c5c3_Var1 = templ.NopComponent
31
32
}
32
33
ctx = templ.ClearChildren(ctx)
33
-
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<!doctype html><html><head><title>Authorization | Micropub and IndieAuth Server Demo</title></head><body><h1>IndieAuth Server Demo: Authorization</h1><p>You received an authorization request from ")
34
+
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<h1>IndieAuth Server Demo: Authorization</h1><p>You received an authorization request from ")
34
35
if templ_7745c5c3_Err != nil {
35
36
return templ_7745c5c3_Err
36
37
}
···
43
44
var templ_7745c5c3_Var2 string
44
45
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(app.Logo)
45
46
if templ_7745c5c3_Err != nil {
46
-
return templ.Error{Err: templ_7745c5c3_Err, FileName: `html/pages/auth.templ`, Line: 21, Col: 72}
47
+
return templ.Error{Err: templ_7745c5c3_Err, FileName: `html/pages/auth.templ`, Line: 16, Col: 70}
47
48
}
48
49
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2))
49
50
if templ_7745c5c3_Err != nil {
···
61
62
var templ_7745c5c3_Var3 string
62
63
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(app.Name)
63
64
if templ_7745c5c3_Err != nil {
64
-
return templ.Error{Err: templ_7745c5c3_Err, FileName: `html/pages/auth.templ`, Line: 24, Col: 26}
65
+
return templ.Error{Err: templ_7745c5c3_Err, FileName: `html/pages/auth.templ`, Line: 19, Col: 24}
65
66
}
66
67
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
67
68
if templ_7745c5c3_Err != nil {
···
74
75
var templ_7745c5c3_Var4 string
75
76
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(app.Author)
76
77
if templ_7745c5c3_Err != nil {
77
-
return templ.Error{Err: templ_7745c5c3_Err, FileName: `html/pages/auth.templ`, Line: 24, Col: 53}
78
+
return templ.Error{Err: templ_7745c5c3_Err, FileName: `html/pages/auth.templ`, Line: 19, Col: 51}
78
79
}
79
80
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
80
81
if templ_7745c5c3_Err != nil {
···
97
98
var templ_7745c5c3_Var5 string
98
99
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(req.ClientID)
99
100
if templ_7745c5c3_Err != nil {
100
-
return templ.Error{Err: templ_7745c5c3_Err, FileName: `html/pages/auth.templ`, Line: 31, Col: 57}
101
+
return templ.Error{Err: templ_7745c5c3_Err, FileName: `html/pages/auth.templ`, Line: 26, Col: 55}
101
102
}
102
103
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
103
104
if templ_7745c5c3_Err != nil {
···
110
111
var templ_7745c5c3_Var6 string
111
112
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(req.RedirectURI)
112
113
if templ_7745c5c3_Err != nil {
113
-
return templ.Error{Err: templ_7745c5c3_Err, FileName: `html/pages/auth.templ`, Line: 32, Col: 58}
114
+
return templ.Error{Err: templ_7745c5c3_Err, FileName: `html/pages/auth.templ`, Line: 27, Col: 56}
114
115
}
115
116
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6))
116
117
if templ_7745c5c3_Err != nil {
···
128
129
var templ_7745c5c3_Var7 string
129
130
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(scope)
130
131
if templ_7745c5c3_Err != nil {
131
-
return templ.Error{Err: templ_7745c5c3_Err, FileName: `html/pages/auth.templ`, Line: 37, Col: 21}
132
+
return templ.Error{Err: templ_7745c5c3_Err, FileName: `html/pages/auth.templ`, Line: 32, Col: 19}
132
133
}
133
134
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7))
134
135
if templ_7745c5c3_Err != nil {
···
146
147
var templ_7745c5c3_Var8 string
147
148
templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(strings.Join(req.Scopes, " "))
148
149
if templ_7745c5c3_Err != nil {
149
-
return templ.Error{Err: templ_7745c5c3_Err, FileName: `html/pages/auth.templ`, Line: 43, Col: 77}
150
+
return templ.Error{Err: templ_7745c5c3_Err, FileName: `html/pages/auth.templ`, Line: 38, Col: 75}
150
151
}
151
152
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8))
152
153
if templ_7745c5c3_Err != nil {
···
159
160
var templ_7745c5c3_Var9 string
160
161
templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.JoinStringErrs(req.RedirectURI)
161
162
if templ_7745c5c3_Err != nil {
162
-
return templ.Error{Err: templ_7745c5c3_Err, FileName: `html/pages/auth.templ`, Line: 44, Col: 70}
163
+
return templ.Error{Err: templ_7745c5c3_Err, FileName: `html/pages/auth.templ`, Line: 39, Col: 68}
163
164
}
164
165
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var9))
165
166
if templ_7745c5c3_Err != nil {
···
172
173
var templ_7745c5c3_Var10 string
173
174
templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs(req.ClientID)
174
175
if templ_7745c5c3_Err != nil {
175
-
return templ.Error{Err: templ_7745c5c3_Err, FileName: `html/pages/auth.templ`, Line: 45, Col: 64}
176
+
return templ.Error{Err: templ_7745c5c3_Err, FileName: `html/pages/auth.templ`, Line: 40, Col: 62}
176
177
}
177
178
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var10))
178
179
if templ_7745c5c3_Err != nil {
···
185
186
var templ_7745c5c3_Var11 string
186
187
templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.JoinStringErrs(req.State)
187
188
if templ_7745c5c3_Err != nil {
188
-
return templ.Error{Err: templ_7745c5c3_Err, FileName: `html/pages/auth.templ`, Line: 46, Col: 57}
189
+
return templ.Error{Err: templ_7745c5c3_Err, FileName: `html/pages/auth.templ`, Line: 41, Col: 55}
189
190
}
190
191
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var11))
191
192
if templ_7745c5c3_Err != nil {
···
198
199
var templ_7745c5c3_Var12 string
199
200
templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.JoinStringErrs(req.CodeChallenge)
200
201
if templ_7745c5c3_Err != nil {
201
-
return templ.Error{Err: templ_7745c5c3_Err, FileName: `html/pages/auth.templ`, Line: 47, Col: 74}
202
+
return templ.Error{Err: templ_7745c5c3_Err, FileName: `html/pages/auth.templ`, Line: 42, Col: 72}
202
203
}
203
204
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var12))
204
205
if templ_7745c5c3_Err != nil {
···
211
212
var templ_7745c5c3_Var13 string
212
213
templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinStringErrs(req.CodeChallengeMethod)
213
214
if templ_7745c5c3_Err != nil {
214
-
return templ.Error{Err: templ_7745c5c3_Err, FileName: `html/pages/auth.templ`, Line: 48, Col: 87}
215
+
return templ.Error{Err: templ_7745c5c3_Err, FileName: `html/pages/auth.templ`, Line: 43, Col: 85}
215
216
}
216
217
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var13))
217
218
if templ_7745c5c3_Err != nil {
218
219
return templ_7745c5c3_Err
219
220
}
220
-
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("\"><p>In a production server, this page could be behind some sort of authentication mechanism, such as username and password, PassKey, etc.</p><button id=\"submit\">Authorize</button></form></body></html>")
221
+
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("\"><input type=\"hidden\" name=\"nonce_id\" value=\"")
222
+
if templ_7745c5c3_Err != nil {
223
+
return templ_7745c5c3_Err
224
+
}
225
+
var templ_7745c5c3_Var14 string
226
+
templ_7745c5c3_Var14, templ_7745c5c3_Err = templ.JoinStringErrs(nonceId)
227
+
if templ_7745c5c3_Err != nil {
228
+
return templ.Error{Err: templ_7745c5c3_Err, FileName: `html/pages/auth.templ`, Line: 46, Col: 56}
229
+
}
230
+
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var14))
231
+
if templ_7745c5c3_Err != nil {
232
+
return templ_7745c5c3_Err
233
+
}
234
+
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("\"> <input type=\"hidden\" name=\"nonce\" value=\"")
235
+
if templ_7745c5c3_Err != nil {
236
+
return templ_7745c5c3_Err
237
+
}
238
+
var templ_7745c5c3_Var15 string
239
+
templ_7745c5c3_Var15, templ_7745c5c3_Err = templ.JoinStringErrs(nonce)
240
+
if templ_7745c5c3_Err != nil {
241
+
return templ.Error{Err: templ_7745c5c3_Err, FileName: `html/pages/auth.templ`, Line: 47, Col: 51}
242
+
}
243
+
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var15))
244
+
if templ_7745c5c3_Err != nil {
245
+
return templ_7745c5c3_Err
246
+
}
247
+
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("\"><p>In a production server, this page could be behind some sort of authentication mechanism, such as username and password, PassKey, etc.</p><button id=\"submit\">Authorize</button></form>")
221
248
if templ_7745c5c3_Err != nil {
222
249
return templ_7745c5c3_Err
223
250
}
+1
-1
main.go
+1
-1
main.go
+14
-29
services/indieauth.go
+14
-29
services/indieauth.go
···
26
26
Server *indieauth.Server
27
27
}
28
28
29
-
// storeAuthorization stores the authorization request and returns a code for it.
30
-
// Something such as JWT tokens could be used in a production environment.
31
29
func (i *IndieAuth) storeAuthorization(req *indieauth.AuthenticationRequest) string {
32
30
code := nanoid.New()
33
31
···
47
45
scopesContextKey contextKey = "scopes"
48
46
)
49
47
50
-
// HandleAuthGET handles the GET method for the authorization endpoint.
51
48
func (i *IndieAuth) HandleAuthGET(w http.ResponseWriter, r *http.Request) {
52
-
// In a production server, this page would usually be protected. In order for
53
-
// the user to authorize this request, they must be authenticated. This could
54
-
// be done in different ways: username/password, passkeys, etc.
55
-
56
-
// Parse the authorization request.
57
49
req, err := i.Server.ParseAuthorization(r)
58
50
if err != nil {
59
51
http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
60
52
return
61
53
}
62
54
63
-
// Do a best effort attempt at fetching more information about the application
64
-
// that we can show to the user. Not all applications provide this sort of
65
-
// information.
66
55
app, _ := i.Server.DiscoverApplicationMetadata(r.Context(), req.ClientID)
67
56
68
-
// Here, we just display a small HTML document where the user has to press
69
-
// to authorize this request. Please note that this template contains a form
70
-
// where we dump all the request information. This makes it possible to reuse
71
-
// [indieauth.Server.ParseAuthorization] when the user authorizes the request.
72
-
layouts.RenderDefault(pages.Auth(req, app)).ServeHTTP(w, r)
57
+
nonceId, nonce := nanoid.New(), nanoid.New()
58
+
storage.NonceCache().Set(nonceId, nonce, 0)
59
+
60
+
layouts.RenderDefault(pages.Auth(req, app, nonceId, nonce)).ServeHTTP(w, r)
73
61
}
74
62
75
-
// HandleAuthPOST handles the POST method for the authorization endpoint.
76
63
func (i *IndieAuth) HandleAuthPOST(w http.ResponseWriter, r *http.Request) {
77
64
i.authorizationCodeExchange(w, r, false)
78
65
}
79
66
80
-
// HandleToken handles the token endpoint. In our case, we only accept the default
81
-
// type which is exchanging an authorization code for a token.
82
67
func (i *IndieAuth) HandleToken(w http.ResponseWriter, r *http.Request) {
83
68
if r.Method != http.MethodPost {
84
69
http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed)
···
103
88
ExpiresIn int64 `json:"expires_in,omitempty"`
104
89
}
105
90
106
-
// authorizationCodeExchange handles the authorization code exchange. It is used by
107
-
// both the authorization handler to exchange the code for the user's profile URL,
108
-
// and by the token endpoint, to exchange the code by a token.
109
91
func (i *IndieAuth) authorizationCodeExchange(w http.ResponseWriter, r *http.Request, withToken bool) {
110
92
if err := r.ParseForm(); err != nil {
111
93
SendErrorJSON(w, http.StatusBadRequest, "invalid_request", err.Error())
···
163
145
}
164
146
165
147
func (i *IndieAuth) HandleAuthApproval(w http.ResponseWriter, r *http.Request) {
166
-
// Parse authorization information. This only works because our authorization page
167
-
// includes all the required information. This can be done in other ways: database,
168
-
// whether temporary or not, cookies, etc.
148
+
id := r.FormValue("nonce_id")
149
+
nonce := r.FormValue("nonce")
150
+
151
+
stored, ok := storage.NonceCache().GetAndDelete(id)
152
+
if !ok {
153
+
SendErrorJSON(w, http.StatusBadRequest, "bad_request", "nonce does not match")
154
+
} else if stored.Value() != nonce {
155
+
SendErrorJSON(w, http.StatusBadRequest, "bad_request", "nonce does not match")
156
+
}
157
+
169
158
req, err := i.Server.ParseAuthorization(r)
170
159
if err != nil {
171
160
SendErrorJSON(w, http.StatusBadRequest, "invalid_request", err.Error())
172
161
return
173
162
}
174
163
175
-
// Generate a random code and persist the information associated to that code.
176
-
// You could do this in other ways: database, or JWT tokens, or both, for example.
177
164
code := i.storeAuthorization(req)
178
165
179
166
// Redirect to client callback.
···
183
170
http.Redirect(w, r, req.RedirectURI+"?"+query.Encode(), http.StatusFound)
184
171
}
185
172
186
-
// MustAuth is a middleware to ensure that the request is authorized. The way this
187
-
// works depends on the implementation. It then stores the scopes in the context.
188
173
func MustAuth(next http.Handler) http.Handler {
189
174
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
190
175
tokenStr := r.Header.Get("Authorization")
+18
-1
storage/cache.go
+18
-1
storage/cache.go
···
8
8
)
9
9
10
10
var authCache *ttlcache.Cache[string, *indieauth.AuthenticationRequest]
11
+
var nonceCache *ttlcache.Cache[string, string]
11
12
12
-
func CleanupAuthCache() {
13
+
func CleanupCaches() {
13
14
AuthCache().Stop()
14
15
}
15
16
···
28
29
29
30
return cache
30
31
}
32
+
33
+
func NonceCache() *ttlcache.Cache[string, string] {
34
+
if nonceCache != nil {
35
+
return nonceCache
36
+
}
37
+
38
+
cache := ttlcache.New(
39
+
ttlcache.WithTTL[string, string](5 * time.Minute),
40
+
)
41
+
42
+
go cache.Start()
43
+
44
+
nonceCache = cache
45
+
46
+
return cache
47
+
}