An experimental IndieWeb site built in Go.

add nonce for replay/csrf protection

Changed files
+118 -92
html
services
storage
+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
··· 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
··· 27 27 28 28 func main() { 29 29 port, profileURL := validateRuntimeConfiguration() 30 - defer storage.CleanupAuthCache() 30 + defer storage.CleanupCaches() 31 31 32 32 storage.GORM().AutoMigrate(&models.Post{}) 33 33
+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
··· 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 + }