Hi… I am well aware that this diff view is very suboptimal. It will be fixed when the refactored server comes along!
Add button to rotate client secret
package main
import (
"fmt"
"net/http"
"net/url"
"strings"
"github.com/go-chi/chi/v5"
)
func manageClient(w http.ResponseWriter, req *http.Request) {
ctx := req.Context()
db := dbFromContext(ctx)
tpl := templateFromContext(ctx)
loginToken := loginTokenFromContext(ctx)
if loginToken == nil {
http.Redirect(w, req, "/login", http.StatusFound)
return
}
me, err := db.FetchUser(ctx, loginToken.User)
if err != nil {
httpError(w, err)
return
} else if !me.Admin {
http.Error(w, "Access denied", http.StatusForbidden)
return
}
client := &Client{Owner: loginToken.User}
if idStr := chi.URLParam(req, "id"); idStr != "" {
id, err := ParseID[*Client](idStr)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
client, err = db.FetchClient(ctx, id)
if err != nil {
httpError(w, err)
return
}
}
if req.Method != http.MethodPost {
data := struct {
Client *Client
}{
Client: client,
}
if err := tpl.ExecuteTemplate(w, "manage-client.html", &data); err != nil {
panic(err)
}
return
}
_ = req.ParseForm()
if _, ok := req.PostForm["delete"]; ok {
if err := db.DeleteClient(ctx, client.ID); err != nil {
httpError(w, err)
return
}
http.Redirect(w, req, "/", http.StatusFound)
return
}
client.ClientName = req.PostFormValue("client_name")
client.ClientURI = req.PostFormValue("client_uri")
client.RedirectURIs = req.PostFormValue("redirect_uris")
isPublic := req.PostFormValue("client_type") == "public"
_, rotate := req.PostForm["rotate"]
for _, s := range strings.Split(client.RedirectURIs, "\n") {
if s == "" {
continue
}
u, err := url.Parse(s)
if err != nil {
var isPublic bool
if client.ID != 0 {
isPublic = client.IsPublic()
} else {
isPublic = req.PostFormValue("client_type") == "public"
}
if !rotate {
client.ClientName = req.PostFormValue("client_name")
client.ClientURI = req.PostFormValue("client_uri")
client.RedirectURIs = req.PostFormValue("redirect_uris")
if err := validateAllowedRedirectURIs(client.RedirectURIs); err != nil {
// TODO: nicer error message
http.Error(w, fmt.Sprintf("Invalid redirect URI %q: %v", s, err), http.StatusBadRequest)
http.Error(w, err.Error(), http.StatusBadRequest)
return }
switch u.Scheme {
case "https":
// ok
case "http":
if u.Host != "localhost" {
http.Error(w, "Only http://localhost is allowed for insecure HTTP URIs", http.StatusBadRequest)
return
}
default:
if !strings.Contains(u.Scheme, ".") {
http.Error(w, "Only private-use URIs referring to domain names are allowed", http.StatusBadRequest)
return
}
}
} var clientSecret string
if client.ID == 0 {
if client.ID == 0 || rotate {
clientSecret, err = client.Generate(isPublic)
if err != nil {
httpError(w, err)
return
}
}
if err := db.StoreClient(ctx, client); err != nil {
httpError(w, err)
return
}
if clientSecret == "" {
http.Redirect(w, req, "/", http.StatusFound)
return
}
data := struct {
ClientID string
ClientSecret string
}{
ClientID: client.ClientID,
ClientSecret: clientSecret,
}
if err := tpl.ExecuteTemplate(w, "client-secret.html", &data); err != nil {
panic(err)
}
}
func validateAllowedRedirectURIs(rawRedirectURIs string) error {
for _, s := range strings.Split(rawRedirectURIs, "\n") {
if s == "" {
continue
}
u, err := url.Parse(s)
if err != nil {
// TODO: nicer error message
return fmt.Errorf("Invalid redirect URI %q: %v", s, err)
}
switch u.Scheme {
case "https":
// ok
case "http":
if u.Host != "localhost" {
return fmt.Errorf("Only http://localhost is allowed for insecure HTTP URIs")
}
default:
if !strings.Contains(u.Scheme, ".") {
return fmt.Errorf("Only private-use URIs referring to domain names are allowed")
}
}
}
return nil
}
func revokeClient(w http.ResponseWriter, req *http.Request) {
ctx := req.Context()
db := dbFromContext(ctx)
id, err := ParseID[*Client](chi.URLParam(req, "id"))
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
loginToken := loginTokenFromContext(ctx)
if loginToken == nil {
http.Redirect(w, req, "/login", http.StatusFound)
return
}
if err := db.RevokeAccessTokens(ctx, id, loginToken.User); err != nil {
httpError(w, err)
return
}
http.Redirect(w, req, "/", http.StatusFound)
}
{{ template "head.html" }}
<main>
<h1>sinwon</h1>
<form method="post" action="">
{{ if .Client.ClientID }}
Client ID: <code>{{ .Client.ClientID }}</code><br>
{{ end }}
Name: <input type="text" name="client_name" value="{{ .Client.ClientName }}"><br>
Website: <input type="url" name="client_uri" value="{{ .Client.ClientURI }}"><br>
Client type:
{{ if .Client.ID }}
{{ if .Client.IsPublic }}
public
{{ else }}
confidential
{{ end }}
<br>
{{ else }}
<br>
<label>
<input type="radio" name="client_type" value="confidential" checked>
Confidential
</label>
<br>
<label>
<input type="radio" name="client_type" value="public">
Public
</label>
<br>
{{ end }}
Redirect URIs:<br>
<textarea name="redirect_uris">{{ .Client.RedirectURIs }}</textarea><br>
<small>The special URI <code>http://localhost</code> matches all loopback interfaces.</small><br>
<a href="/"><button type="button">Cancel</button></a>
<button type="submit">
{{ if .Client.ID }}
Update client
{{ else }}
Create client
{{ end }}
</button>
{{ if .Client.ID }}
{{ if not .Client.IsPublic }}
<button type="submit" name="rotate">Rotate client secret</button>
{{ end }}
<button type="submit" name="delete">Delete client</button>
{{ end }}
</form>
</main>
{{ template "foot.html" }}