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 package pages 2 3 - import "strings" 4 5 - import "go.hacdias.com/indielib/indieauth" 6 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> 15 16 - <p> 17 - You received an authorization request from 18 19 - if app != nil { 20 - if len(app.Logo) > 0 { 21 - <img style="width: 1em; vertical-align: middle" src={ app.Logo } /> 22 - } 23 24 - <strong>{ app.Name }</strong> by { app.Author }: 25 - } else { 26 - the following client: 27 - } 28 - </p> 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> 34 35 - <p>For the following scopes: 36 - for _, scope := range req.Scopes { 37 - <code>{ scope }</code> 38 - } 39 - .</p> 40 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 }> 49 50 - <p>In a production server, this page could be behind some sort of authentication mechanism, such as username and password, PassKey, etc.</p> 51 52 - <button id="submit">Authorize</button> 53 - </form> 54 - </body> 55 - </html> 56 }
··· 1 package pages 2 3 + import ( 4 + "strings" 5 + "go.hacdias.com/indielib/indieauth" 6 + ) 7 8 + templ Auth( req *indieauth.AuthenticationRequest, app *indieauth.ApplicationMetadata, nonceId string, nonce string ) { 9 + <h1>IndieAuth Server Demo: Authorization</h1> 10 11 + <p> 12 + You received an authorization request from 13 14 + if app != nil { 15 + if len(app.Logo) > 0 { 16 + <img style="width: 1em; vertical-align: middle" src={ app.Logo } /> 17 + } 18 19 + <strong>{ app.Name }</strong> by { app.Author }: 20 + } else { 21 + the following client: 22 + } 23 + </p> 24 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 30 + <p>For the following scopes: 31 + for _, scope := range req.Scopes { 32 + <code>{ scope }</code> 33 + } 34 + .</p> 35 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 }/> 44 45 + // CSRF protections 46 + <input type="hidden" name="nonce_id" value={ nonceId }/> 47 + <input type="hidden" name="nonce" value={ nonce }/> 48 49 + <p>In a production server, this page could be behind some sort of authentication mechanism, such as username and password, PassKey, etc.</p> 50 51 + <button id="submit">Authorize</button> 52 + </form> 53 }
+45 -18
html/pages/auth_templ.go
··· 8 import "github.com/a-h/templ" 9 import templruntime "github.com/a-h/templ/runtime" 10 11 - import "strings" 12 - 13 - import "go.hacdias.com/indielib/indieauth" 14 15 - func Auth(req *indieauth.AuthenticationRequest, app *indieauth.ApplicationMetadata) templ.Component { 16 return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { 17 templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context 18 templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) ··· 30 templ_7745c5c3_Var1 = templ.NopComponent 31 } 32 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 if templ_7745c5c3_Err != nil { 35 return templ_7745c5c3_Err 36 } ··· 43 var templ_7745c5c3_Var2 string 44 templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(app.Logo) 45 if templ_7745c5c3_Err != nil { 46 - return templ.Error{Err: templ_7745c5c3_Err, FileName: `html/pages/auth.templ`, Line: 21, Col: 72} 47 } 48 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2)) 49 if templ_7745c5c3_Err != nil { ··· 61 var templ_7745c5c3_Var3 string 62 templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(app.Name) 63 if templ_7745c5c3_Err != nil { 64 - return templ.Error{Err: templ_7745c5c3_Err, FileName: `html/pages/auth.templ`, Line: 24, Col: 26} 65 } 66 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3)) 67 if templ_7745c5c3_Err != nil { ··· 74 var templ_7745c5c3_Var4 string 75 templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(app.Author) 76 if templ_7745c5c3_Err != nil { 77 - return templ.Error{Err: templ_7745c5c3_Err, FileName: `html/pages/auth.templ`, Line: 24, Col: 53} 78 } 79 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4)) 80 if templ_7745c5c3_Err != nil { ··· 97 var templ_7745c5c3_Var5 string 98 templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(req.ClientID) 99 if templ_7745c5c3_Err != nil { 100 - return templ.Error{Err: templ_7745c5c3_Err, FileName: `html/pages/auth.templ`, Line: 31, Col: 57} 101 } 102 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5)) 103 if templ_7745c5c3_Err != nil { ··· 110 var templ_7745c5c3_Var6 string 111 templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(req.RedirectURI) 112 if templ_7745c5c3_Err != nil { 113 - return templ.Error{Err: templ_7745c5c3_Err, FileName: `html/pages/auth.templ`, Line: 32, Col: 58} 114 } 115 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6)) 116 if templ_7745c5c3_Err != nil { ··· 128 var templ_7745c5c3_Var7 string 129 templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(scope) 130 if templ_7745c5c3_Err != nil { 131 - return templ.Error{Err: templ_7745c5c3_Err, FileName: `html/pages/auth.templ`, Line: 37, Col: 21} 132 } 133 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7)) 134 if templ_7745c5c3_Err != nil { ··· 146 var templ_7745c5c3_Var8 string 147 templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(strings.Join(req.Scopes, " ")) 148 if templ_7745c5c3_Err != nil { 149 - return templ.Error{Err: templ_7745c5c3_Err, FileName: `html/pages/auth.templ`, Line: 43, Col: 77} 150 } 151 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8)) 152 if templ_7745c5c3_Err != nil { ··· 159 var templ_7745c5c3_Var9 string 160 templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.JoinStringErrs(req.RedirectURI) 161 if templ_7745c5c3_Err != nil { 162 - return templ.Error{Err: templ_7745c5c3_Err, FileName: `html/pages/auth.templ`, Line: 44, Col: 70} 163 } 164 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var9)) 165 if templ_7745c5c3_Err != nil { ··· 172 var templ_7745c5c3_Var10 string 173 templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs(req.ClientID) 174 if templ_7745c5c3_Err != nil { 175 - return templ.Error{Err: templ_7745c5c3_Err, FileName: `html/pages/auth.templ`, Line: 45, Col: 64} 176 } 177 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var10)) 178 if templ_7745c5c3_Err != nil { ··· 185 var templ_7745c5c3_Var11 string 186 templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.JoinStringErrs(req.State) 187 if templ_7745c5c3_Err != nil { 188 - return templ.Error{Err: templ_7745c5c3_Err, FileName: `html/pages/auth.templ`, Line: 46, Col: 57} 189 } 190 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var11)) 191 if templ_7745c5c3_Err != nil { ··· 198 var templ_7745c5c3_Var12 string 199 templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.JoinStringErrs(req.CodeChallenge) 200 if templ_7745c5c3_Err != nil { 201 - return templ.Error{Err: templ_7745c5c3_Err, FileName: `html/pages/auth.templ`, Line: 47, Col: 74} 202 } 203 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var12)) 204 if templ_7745c5c3_Err != nil { ··· 211 var templ_7745c5c3_Var13 string 212 templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinStringErrs(req.CodeChallengeMethod) 213 if templ_7745c5c3_Err != nil { 214 - return templ.Error{Err: templ_7745c5c3_Err, FileName: `html/pages/auth.templ`, Line: 48, Col: 87} 215 } 216 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var13)) 217 if templ_7745c5c3_Err != nil { 218 return templ_7745c5c3_Err 219 } 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 if templ_7745c5c3_Err != nil { 222 return templ_7745c5c3_Err 223 }
··· 8 import "github.com/a-h/templ" 9 import templruntime "github.com/a-h/templ/runtime" 10 11 + import ( 12 + "go.hacdias.com/indielib/indieauth" 13 + "strings" 14 + ) 15 16 + func Auth(req *indieauth.AuthenticationRequest, app *indieauth.ApplicationMetadata, nonceId string, nonce string) templ.Component { 17 return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { 18 templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context 19 templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) ··· 31 templ_7745c5c3_Var1 = templ.NopComponent 32 } 33 ctx = templ.ClearChildren(ctx) 34 + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<h1>IndieAuth Server Demo: Authorization</h1><p>You received an authorization request from ") 35 if templ_7745c5c3_Err != nil { 36 return templ_7745c5c3_Err 37 } ··· 44 var templ_7745c5c3_Var2 string 45 templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(app.Logo) 46 if templ_7745c5c3_Err != nil { 47 + return templ.Error{Err: templ_7745c5c3_Err, FileName: `html/pages/auth.templ`, Line: 16, Col: 70} 48 } 49 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2)) 50 if templ_7745c5c3_Err != nil { ··· 62 var templ_7745c5c3_Var3 string 63 templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(app.Name) 64 if templ_7745c5c3_Err != nil { 65 + return templ.Error{Err: templ_7745c5c3_Err, FileName: `html/pages/auth.templ`, Line: 19, Col: 24} 66 } 67 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3)) 68 if templ_7745c5c3_Err != nil { ··· 75 var templ_7745c5c3_Var4 string 76 templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(app.Author) 77 if templ_7745c5c3_Err != nil { 78 + return templ.Error{Err: templ_7745c5c3_Err, FileName: `html/pages/auth.templ`, Line: 19, Col: 51} 79 } 80 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4)) 81 if templ_7745c5c3_Err != nil { ··· 98 var templ_7745c5c3_Var5 string 99 templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(req.ClientID) 100 if templ_7745c5c3_Err != nil { 101 + return templ.Error{Err: templ_7745c5c3_Err, FileName: `html/pages/auth.templ`, Line: 26, Col: 55} 102 } 103 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5)) 104 if templ_7745c5c3_Err != nil { ··· 111 var templ_7745c5c3_Var6 string 112 templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(req.RedirectURI) 113 if templ_7745c5c3_Err != nil { 114 + return templ.Error{Err: templ_7745c5c3_Err, FileName: `html/pages/auth.templ`, Line: 27, Col: 56} 115 } 116 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6)) 117 if templ_7745c5c3_Err != nil { ··· 129 var templ_7745c5c3_Var7 string 130 templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(scope) 131 if templ_7745c5c3_Err != nil { 132 + return templ.Error{Err: templ_7745c5c3_Err, FileName: `html/pages/auth.templ`, Line: 32, Col: 19} 133 } 134 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7)) 135 if templ_7745c5c3_Err != nil { ··· 147 var templ_7745c5c3_Var8 string 148 templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(strings.Join(req.Scopes, " ")) 149 if templ_7745c5c3_Err != nil { 150 + return templ.Error{Err: templ_7745c5c3_Err, FileName: `html/pages/auth.templ`, Line: 38, Col: 75} 151 } 152 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8)) 153 if templ_7745c5c3_Err != nil { ··· 160 var templ_7745c5c3_Var9 string 161 templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.JoinStringErrs(req.RedirectURI) 162 if templ_7745c5c3_Err != nil { 163 + return templ.Error{Err: templ_7745c5c3_Err, FileName: `html/pages/auth.templ`, Line: 39, Col: 68} 164 } 165 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var9)) 166 if templ_7745c5c3_Err != nil { ··· 173 var templ_7745c5c3_Var10 string 174 templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs(req.ClientID) 175 if templ_7745c5c3_Err != nil { 176 + return templ.Error{Err: templ_7745c5c3_Err, FileName: `html/pages/auth.templ`, Line: 40, Col: 62} 177 } 178 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var10)) 179 if templ_7745c5c3_Err != nil { ··· 186 var templ_7745c5c3_Var11 string 187 templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.JoinStringErrs(req.State) 188 if templ_7745c5c3_Err != nil { 189 + return templ.Error{Err: templ_7745c5c3_Err, FileName: `html/pages/auth.templ`, Line: 41, Col: 55} 190 } 191 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var11)) 192 if templ_7745c5c3_Err != nil { ··· 199 var templ_7745c5c3_Var12 string 200 templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.JoinStringErrs(req.CodeChallenge) 201 if templ_7745c5c3_Err != nil { 202 + return templ.Error{Err: templ_7745c5c3_Err, FileName: `html/pages/auth.templ`, Line: 42, Col: 72} 203 } 204 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var12)) 205 if templ_7745c5c3_Err != nil { ··· 212 var templ_7745c5c3_Var13 string 213 templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinStringErrs(req.CodeChallengeMethod) 214 if templ_7745c5c3_Err != nil { 215 + return templ.Error{Err: templ_7745c5c3_Err, FileName: `html/pages/auth.templ`, Line: 43, Col: 85} 216 } 217 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var13)) 218 if templ_7745c5c3_Err != nil { 219 return templ_7745c5c3_Err 220 } 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>") 248 if templ_7745c5c3_Err != nil { 249 return templ_7745c5c3_Err 250 }
+1 -1
main.go
··· 27 28 func main() { 29 port, profileURL := validateRuntimeConfiguration() 30 - defer storage.CleanupAuthCache() 31 32 storage.GORM().AutoMigrate(&models.Post{}) 33
··· 27 28 func main() { 29 port, profileURL := validateRuntimeConfiguration() 30 + defer storage.CleanupCaches() 31 32 storage.GORM().AutoMigrate(&models.Post{}) 33
+14 -29
services/indieauth.go
··· 26 Server *indieauth.Server 27 } 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 func (i *IndieAuth) storeAuthorization(req *indieauth.AuthenticationRequest) string { 32 code := nanoid.New() 33 ··· 47 scopesContextKey contextKey = "scopes" 48 ) 49 50 - // HandleAuthGET handles the GET method for the authorization endpoint. 51 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 req, err := i.Server.ParseAuthorization(r) 58 if err != nil { 59 http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) 60 return 61 } 62 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 app, _ := i.Server.DiscoverApplicationMetadata(r.Context(), req.ClientID) 67 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) 73 } 74 75 - // HandleAuthPOST handles the POST method for the authorization endpoint. 76 func (i *IndieAuth) HandleAuthPOST(w http.ResponseWriter, r *http.Request) { 77 i.authorizationCodeExchange(w, r, false) 78 } 79 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 func (i *IndieAuth) HandleToken(w http.ResponseWriter, r *http.Request) { 83 if r.Method != http.MethodPost { 84 http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed) ··· 103 ExpiresIn int64 `json:"expires_in,omitempty"` 104 } 105 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 func (i *IndieAuth) authorizationCodeExchange(w http.ResponseWriter, r *http.Request, withToken bool) { 110 if err := r.ParseForm(); err != nil { 111 SendErrorJSON(w, http.StatusBadRequest, "invalid_request", err.Error()) ··· 163 } 164 165 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. 169 req, err := i.Server.ParseAuthorization(r) 170 if err != nil { 171 SendErrorJSON(w, http.StatusBadRequest, "invalid_request", err.Error()) 172 return 173 } 174 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 code := i.storeAuthorization(req) 178 179 // Redirect to client callback. ··· 183 http.Redirect(w, r, req.RedirectURI+"?"+query.Encode(), http.StatusFound) 184 } 185 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 func MustAuth(next http.Handler) http.Handler { 189 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 190 tokenStr := r.Header.Get("Authorization")
··· 26 Server *indieauth.Server 27 } 28 29 func (i *IndieAuth) storeAuthorization(req *indieauth.AuthenticationRequest) string { 30 code := nanoid.New() 31 ··· 45 scopesContextKey contextKey = "scopes" 46 ) 47 48 func (i *IndieAuth) HandleAuthGET(w http.ResponseWriter, r *http.Request) { 49 req, err := i.Server.ParseAuthorization(r) 50 if err != nil { 51 http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) 52 return 53 } 54 55 app, _ := i.Server.DiscoverApplicationMetadata(r.Context(), req.ClientID) 56 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) 61 } 62 63 func (i *IndieAuth) HandleAuthPOST(w http.ResponseWriter, r *http.Request) { 64 i.authorizationCodeExchange(w, r, false) 65 } 66 67 func (i *IndieAuth) HandleToken(w http.ResponseWriter, r *http.Request) { 68 if r.Method != http.MethodPost { 69 http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed) ··· 88 ExpiresIn int64 `json:"expires_in,omitempty"` 89 } 90 91 func (i *IndieAuth) authorizationCodeExchange(w http.ResponseWriter, r *http.Request, withToken bool) { 92 if err := r.ParseForm(); err != nil { 93 SendErrorJSON(w, http.StatusBadRequest, "invalid_request", err.Error()) ··· 145 } 146 147 func (i *IndieAuth) HandleAuthApproval(w http.ResponseWriter, r *http.Request) { 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 + 158 req, err := i.Server.ParseAuthorization(r) 159 if err != nil { 160 SendErrorJSON(w, http.StatusBadRequest, "invalid_request", err.Error()) 161 return 162 } 163 164 code := i.storeAuthorization(req) 165 166 // Redirect to client callback. ··· 170 http.Redirect(w, r, req.RedirectURI+"?"+query.Encode(), http.StatusFound) 171 } 172 173 func MustAuth(next http.Handler) http.Handler { 174 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 175 tokenStr := r.Header.Get("Authorization")
+18 -1
storage/cache.go
··· 8 ) 9 10 var authCache *ttlcache.Cache[string, *indieauth.AuthenticationRequest] 11 12 - func CleanupAuthCache() { 13 AuthCache().Stop() 14 } 15 ··· 28 29 return cache 30 }
··· 8 ) 9 10 var authCache *ttlcache.Cache[string, *indieauth.AuthenticationRequest] 11 + var nonceCache *ttlcache.Cache[string, string] 12 13 + func CleanupCaches() { 14 AuthCache().Stop() 15 } 16 ··· 29 30 return cache 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 + }