Commit cfd94ca

mo khan <mo@mokhan.ca>
2022-04-29 19:44:44
feat: add RFC-7591 dynamic client registration
1 parent 774a911
pkg/web/default.go
@@ -3,5 +3,5 @@ package web
 import "net/http"
 
 func (h *HttpContext) Default(w http.ResponseWriter, r *http.Request) {
-	w.WriteHeader(http.StatusOK)
+	w.WriteHeader(http.StatusNotFound)
 }
pkg/web/http_context.go
@@ -25,6 +25,7 @@ func (h *HttpContext) Router() *http.ServeMux {
 	mux.Handle("/", http.HandlerFunc(h.Default))
 	mux.Handle("/.well-known/", h.wellKnownMux())
 	mux.Handle("/authorize", http.HandlerFunc(h.Authorize))
+	mux.Handle("/register", http.HandlerFunc(h.Register))
 	mux.Handle("/revoke", http.HandlerFunc(http.NotFound))
 	mux.Handle("/token", http.HandlerFunc(h.Token))
 	mux.Handle("/userinfo", http.HandlerFunc(http.NotFound))
pkg/web/register.go
@@ -0,0 +1,50 @@
+package web
+
+import (
+	"encoding/json"
+	"net/http"
+	"time"
+
+	"github.com/hashicorp/uuid"
+)
+
+type ClientRegistrationRequest struct {
+	RedirectUris            []string `json:"redirect_uris"`
+	ClientName              string   `json:"client_name"`
+	TokenEndpointAuthMethod string   `json:"token_endpoint_auth_method"`
+	LogoUri                 string   `json:"logo_uri"`
+	JwksUri                 string   `json:"jwks_uri"`
+}
+
+type ClientInformationResponse struct {
+	ClientId                string   `json:"client_id"`
+	ClientSecret            string   `json:"client_secret"`
+	ClientIdIssuedAt        int64    `json:"client_id_issued_at"`
+	ClientSecretExpiresAt   int64    `json:"client_secret_expires_at"`
+	RedirectUris            []string `json:"redirect_uris"`
+	GrantTypes              []string `json:"grant_types"`
+	ClientName              string   `json:"client_name"`
+	TokenEndpointAuthMethod string   `json:"token_endpoint_auth_method"`
+	LogoUri                 string   `json:"logo_uri"`
+	JwksUri                 string   `json:"jwks_uri"`
+}
+
+func (h *HttpContext) Register(w http.ResponseWriter, r *http.Request) {
+	var request ClientRegistrationRequest
+	json.NewDecoder(r.Body).Decode(&request)
+
+	expiresAt := time.Now().Add(time.Duration(1) * time.Hour)
+	response := ClientInformationResponse{
+		ClientId:              uuid.GenerateUUID(),
+		ClientSecret:          uuid.GenerateUUID(),
+		ClientIdIssuedAt:      time.Now().Unix(),
+		ClientSecretExpiresAt: expiresAt.Unix(),
+		RedirectUris:          request.RedirectUris,
+	}
+
+	w.WriteHeader(http.StatusCreated)
+	w.Header().Set("Content-Type", "application/json")
+	w.Header().Set("Cache-Control", "no-store")
+	w.Header().Set("Pragma", "no-cache")
+	json.NewEncoder(w).Encode(&response)
+}
pkg/web/register_test.go
@@ -0,0 +1,41 @@
+package web
+
+import (
+	"encoding/json"
+	"net/http"
+	"net/http/httptest"
+	"strings"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestRegister(t *testing.T) {
+	srv := NewHttpContext("https://example.com", []byte{})
+
+	t.Run("POST /register", func(t *testing.T) {
+		w := httptest.NewRecorder()
+
+		body := `{"redirect_uris": ["https://client.example.org/callback"], "client_name": "My Client", "token_endpoint_auth_method": "client_secret_basic", "logo_uri": "https://client.example.org/logo.png", "jwks_uri": "https://client.example.org/my_public_keys.jwks"}`
+		r := httptest.NewRequest("POST", "/register", strings.NewReader(body))
+		r.Header.Set("Content-Type", "application/json")
+		r.Header.Set("Accept", "application/json")
+
+		srv.Router().ServeHTTP(w, r)
+
+		assert.Equal(t, http.StatusCreated, w.Result().StatusCode)
+		assert.Equal(t, "application/json", w.HeaderMap.Get("Content-Type"))
+		assert.Equal(t, "no-store", w.HeaderMap.Get("Cache-Control"))
+		assert.Equal(t, "no-cache", w.HeaderMap.Get("Pragma"))
+
+		var x ClientInformationResponse
+		json.NewDecoder(w.Body).Decode(&x)
+
+		assert.NotEmpty(t, x.ClientId)
+		assert.NotEmpty(t, x.ClientSecret)
+		assert.NotEmpty(t, x.ClientIdIssuedAt)
+		assert.NotEmpty(t, x.ClientSecretExpiresAt)
+		assert.Equal(t, 1, len(x.RedirectUris))
+		assert.Equal(t, "https://client.example.org/callback", x.RedirectUris[0])
+	})
+}