Lindenii Project Forge
Login

server

Vireo IdP server

Hi… I am well aware that this diff view is very suboptimal. It will be fixed when the refactored server comes along!

Commit info
ID
63aed4af8bd1dac9ca730b31ced517c54ca49388
Author
Author date
Mon, 19 Feb 2024 17:27:10 +0100
Committer
Committer date
Mon, 19 Feb 2024 17:27:10 +0100
Actions
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" }}