Warning: Due to various recent migrations, viewing non-HEAD refs may be broken.
/client.go (raw)
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 normalized, err := normalizeClientPKCERequirement(client.PKCERequirement); err == nil {
client.PKCERequirement = normalized
} else {
client.PKCERequirement = pkceRequirementNone
}
if req.Method != http.MethodPost {
data := struct {
TemplateBaseData
Client *Client
}{
Client: client,
}
tpl.MustExecuteTemplate(req.Context(), w, "manage-client.html", &data)
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
}
_, rotate := req.PostForm["rotate"]
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")
pkceRequirement := req.PostFormValue("pkce_requirement")
if !isPublic {
pkceRequirement = pkceRequirementNone
}
normalizedRequirement, err := normalizeClientPKCERequirement(pkceRequirement)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
client.PKCERequirement = normalizedRequirement
if err := validateAllowedRedirectURIs(client.RedirectURIs); err != nil {
// TODO: nicer error message
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
}
var clientSecret string
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 {
TemplateBaseData
ClientID string
ClientSecret string
}{
ClientID: client.ClientID,
ClientSecret: clientSecret,
}
tpl.MustExecuteTemplate(req.Context(), w, "client-secret.html", &data)
}
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":
// insecure but let's just trust the admin
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.RevokeClientUser(ctx, id, loginToken.User); err != nil {
httpError(w, err)
return
}
http.Redirect(w, req, "/", http.StatusFound)
}