Commit ed7a633

mo khan <mo@mokhan.ca>
2022-04-20 23:37:42
build a tiny oidc provider
1 parent 30fa85a
go.mod
@@ -1,10 +1,20 @@
-module git.mokhan.ca/xlgmokha/oauth
+module mokhan.ca/xlgmokha/oauth
 
 go 1.18
 
 require (
-	github.com/davecgh/go-spew v1.1.0 // indirect
-	github.com/pmezard/go-difflib v1.0.0 // indirect
-	github.com/stretchr/testify v1.7.1 // indirect
-	gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c // indirect
+	github.com/golang-jwt/jwt v3.2.2+incompatible
+	github.com/hashicorp/uuid v0.0.0-20160311170451-ebb0a03e909c
+	github.com/lestrrat-go/jwx/v2 v2.0.0-beta1
+)
+
+require (
+	github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 // indirect
+	github.com/goccy/go-json v0.9.6 // indirect
+	github.com/lestrrat-go/blackmagic v1.0.1 // indirect
+	github.com/lestrrat-go/httpcc v1.0.1 // indirect
+	github.com/lestrrat-go/httprc v1.0.1 // indirect
+	github.com/lestrrat-go/iter v1.0.2 // indirect
+	github.com/lestrrat-go/option v1.0.0 // indirect
+	golang.org/x/crypto v0.0.0-20220214200702-86341886e292 // indirect
 )
go.sum
@@ -1,10 +1,41 @@
 github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/decred/dcrd/crypto/blake256 v1.0.0/go.mod h1:sQl2p6Y26YV+ZOcSTP6thNdn47hh8kt6rqSlvmrXFAc=
+github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 h1:YLtO71vCjJRCBcrPMtQ9nqBsqpA1m5sE92cU+pd5Mcc=
+github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1/go.mod h1:hyedUtir6IdtD/7lIxGeCxkaw7y45JueMRL4DIyJDKs=
+github.com/goccy/go-json v0.9.6 h1:5/4CtRQdtsX0sal8fdVhTaiMN01Ri8BExZZ8iRmHQ6E=
+github.com/goccy/go-json v0.9.6/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
+github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY=
+github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I=
+github.com/hashicorp/uuid v0.0.0-20160311170451-ebb0a03e909c h1:nQcv325vxv2fFHJsOt53eSRf1eINt6vOdYUFfXs4rgk=
+github.com/hashicorp/uuid v0.0.0-20160311170451-ebb0a03e909c/go.mod h1:fHzc09UnyJyqyW+bFuq864eh+wC7dj65aXmXLRe5to0=
+github.com/lestrrat-go/blackmagic v1.0.1 h1:lS5Zts+5HIC/8og6cGHb0uCcNCa3OUt1ygh3Qz2Fe80=
+github.com/lestrrat-go/blackmagic v1.0.1/go.mod h1:UrEqBzIR2U6CnzVyUtfM6oZNMt/7O7Vohk2J0OGSAtU=
+github.com/lestrrat-go/httpcc v1.0.1 h1:ydWCStUeJLkpYyjLDHihupbn2tYmZ7m22BGkcvZZrIE=
+github.com/lestrrat-go/httpcc v1.0.1/go.mod h1:qiltp3Mt56+55GPVCbTdM9MlqhvzyuL6W/NMDA8vA5E=
+github.com/lestrrat-go/httprc v1.0.1 h1:Cnc4NxIySph38pQPzKbjg5OkKsGR/Cf5xcWt5OlSUDI=
+github.com/lestrrat-go/httprc v1.0.1/go.mod h1:5Ml+nB++j6IC0e6LzefJnrpMQDKgDwDCaIQQzhbqhJM=
+github.com/lestrrat-go/iter v1.0.2 h1:gMXo1q4c2pHmC3dn8LzRhJfP1ceCbgSiT9lUydIzltI=
+github.com/lestrrat-go/iter v1.0.2/go.mod h1:Momfcq3AnRlRjI5b5O8/G5/BvpzrhoFTZcn06fEOPt4=
+github.com/lestrrat-go/jwx/v2 v2.0.0-beta1 h1:zVHfLjzsWPjAF21CdoTCV3x7X3zixSi3kTXBLmbSI4Y=
+github.com/lestrrat-go/jwx/v2 v2.0.0-beta1/go.mod h1:G8yN95iNzKc/y82IpU2MW+mOeGrDm5j773pE5M0w/7w=
+github.com/lestrrat-go/option v1.0.0 h1:WqAWL8kh8VcSoD6xjSH34/1m8yxluXQbDeKNfvFeEO4=
+github.com/lestrrat-go/option v1.0.0/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I=
 github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
 github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
 github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
 github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY=
 github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+golang.org/x/crypto v0.0.0-20220214200702-86341886e292 h1:f+lwQ+GtmgoY+A2YaQxlSOnDjXcQ7ZRLWOHbC6HtRqE=
+golang.org/x/crypto v0.0.0-20220214200702-86341886e292/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
+golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
+golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
+golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
 gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
main.go
@@ -1,20 +1,181 @@
 package main
 
 import (
+	"crypto/x509"
+	"encoding/json"
+	"encoding/pem"
 	"fmt"
+	"io/ioutil"
 	"log"
 	"net/http"
+	"os"
+	"text/template"
+	"time"
+
+	"github.com/golang-jwt/jwt"
+	"github.com/hashicorp/uuid"
+	"github.com/lestrrat-go/jwx/v2/jwk"
 )
 
-func health(w http.ResponseWriter, req *http.Request) {
-	fmt.Fprintf(w, "OK")
+type AuthorizationRequest struct {
+	ResponseType string
+	Scope        string
+	ClientId     string
+	State        string
+	RedirectUri  string
+	Nonce        string
+}
+
+type TokenRequest struct {
+	GrantType   string
+	Code        string
+	RedirectUri string
+}
+
+type TokenResponse struct {
+	AccessToken  string
+	TokenType    string
+	RefreshToken string
+	ExpiresIn    int
+	IdToken      string
+}
+
+var (
+	tokens = map[string]string{}
+)
+
+func createIdToken(clientId string) string {
+	now := time.Now()
+	if clientId == "" {
+		clientId = "clientId"
+	}
+	expiresAt := now.Add(time.Hour * time.Duration(1))
+
+	host, ok := os.LookupEnv("HOST")
+	if !ok {
+		host = "http://localhost:8282"
+	}
+	idToken := jwt.NewWithClaims(jwt.SigningMethodRS256, &jwt.StandardClaims{
+		Issuer:    host,
+		Subject:   "1",
+		Audience:  clientId,
+		ExpiresAt: expiresAt.Unix(),
+		NotBefore: now.Unix(),
+		IssuedAt:  now.Unix(),
+		Id:        uuid.GenerateUUID(),
+	})
+
+	keyData, _ := ioutil.ReadFile("insecure.pem")
+	key, _ := jwt.ParseRSAPrivateKeyFromPEM(keyData)
+	signedIdToken, _ := idToken.SignedString(key)
+	return signedIdToken
+}
+
+func handler(w http.ResponseWriter, r *http.Request) {
+	if r.URL.Path == "/" && r.Method == "GET" {
+		w.WriteHeader(http.StatusOK)
+		fmt.Fprintf(w, "Hello, world!\n")
+	} else if r.URL.Path == "/authorize" && r.Method == "GET" {
+		responseType := r.FormValue("response_type")
+		if responseType == "code" {
+			// Authorization Code Flow https://openid.net/specs/openid-connect-core-1_0.html#CodeFlowAuth
+			ar := &AuthorizationRequest{
+				ResponseType: r.FormValue("response_type"),
+				Scope:        r.FormValue("scope"),
+				ClientId:     r.FormValue("client_id"),
+				State:        r.FormValue("state"),
+				RedirectUri:  r.FormValue("redirect_uri"),
+			}
+			code := uuid.GenerateUUID()
+			tokens[code] = uuid.GenerateUUID()
+			url := fmt.Sprintf("%s?code=%s&state=%s", ar.RedirectUri, code, ar.State)
+			http.Redirect(w, r, url, 302)
+		} else if responseType == "id_token token" || responseType == "id_token" {
+			// Implicit Flow https://openid.net/specs/openid-connect-core-1_0.html#ImplicitFlowAuth
+			ar := &AuthorizationRequest{
+				ResponseType: r.FormValue("response_type"),
+				RedirectUri:  r.FormValue("redirect_uri"),
+				Nonce:        r.FormValue("nonce"),
+			}
+			idToken := createIdToken(r.FormValue("client_id"))
+			url := fmt.Sprintf("%s?access_token=example&token_type=bearer&id_token=%s&expires_in=3600&state=%s", ar.RedirectUri, idToken, ar.State)
+			http.Redirect(w, r, url, 302)
+		} else if responseType == "code id_token" || responseType == "code token" || responseType == "code id_token token" {
+			// Hybrid Flow https://openid.net/specs/openid-connect-core-1_0.html#HybridFlowAuth
+			w.WriteHeader(http.StatusNotImplemented)
+		} else {
+			w.WriteHeader(http.StatusNotFound)
+			fmt.Fprintf(w, "Not Found\n")
+		}
+	} else if r.URL.Path == "/token" && r.Method == "POST" {
+		tr := &TokenRequest{
+			GrantType:   r.FormValue("grant_type"),
+			Code:        r.FormValue("code"),
+			RedirectUri: r.FormValue("redirect_uri"),
+		}
+		if tr.GrantType == "authorization_code" {
+			// Authorization Code Flow https://openid.net/specs/openid-connect-core-1_0.html#CodeFlowAuth
+			r := &TokenResponse{
+				AccessToken:  tokens[tr.Code],
+				TokenType:    "Bearer",
+				RefreshToken: "TODO::",
+				ExpiresIn:    3600,
+				IdToken:      createIdToken(r.FormValue("client_id")),
+			}
+
+			w.Header().Set("Content-Type", "application/json")
+			w.Header().Set("Cache-Control", "no-store")
+			w.Header().Set("Pragma", "no-cache")
+			fmt.Fprintf(w, `{"access_token": "%s","token_type": "%s","refresh_token": "%s","expires_in": %d,"id_token": "%s"}`, r.AccessToken, r.TokenType, r.RefreshToken, r.ExpiresIn, r.IdToken)
+		} else {
+			w.WriteHeader(http.StatusNotFound)
+			fmt.Fprintf(w, "Not Found\n")
+		}
+	} else if r.URL.Path == "/.well-known/openid-configuration" {
+		w.Header().Set("Content-Type", "application/json")
+		data, _ := ioutil.ReadFile("openid-configuration.json")
+		tmpl, _ := template.New("test").Parse(string(data))
+		host, ok := os.LookupEnv("HOST")
+		if !ok {
+			host = "http://localhost:8282"
+		}
+		tmpl.Execute(w, struct{ Host string }{Host: host})
+	} else if r.URL.Path == "/userinfo" {
+		w.WriteHeader(http.StatusNotImplemented)
+	} else if r.URL.Path == "/.well-known/jwks.json" {
+		w.Header().Set("Content-Type", "application/json")
+		keyData, _ := ioutil.ReadFile("insecure.pem")
+		privatePem, _ := pem.Decode(keyData)
+		parsedKey, _ := x509.ParsePKCS1PrivateKey(privatePem.Bytes)
+		key, _ := jwk.FromRaw(parsedKey)
+		pubKey, _ := jwk.PublicKeyOf(key)
+		pubKey.Set(jwk.KeyIDKey, "X")
+		pubKey.Set(jwk.KeyUsageKey, "sig")
+
+		set := jwk.NewSet()
+		set.Add(pubKey)
+		json.NewEncoder(w).Encode(set)
+	} else if r.URL.Path == "/revoke" {
+		w.WriteHeader(http.StatusNotImplemented)
+	} else {
+		w.WriteHeader(http.StatusNotFound)
+		fmt.Fprintf(w, "Not Found\n")
+	}
 }
 
 func main() {
-	server := NewServer()
+	log.Println("Starting server, listening on port 8282.")
 
-	http.Handle("/", http.FileServer(http.Dir("public")))
-	http.HandleFunc("/api/", server.ServeHTTP)
+	server := &http.Server{
+		Addr:         ":8282",
+		Handler:      http.HandlerFunc(handler),
+		ReadTimeout:  0,
+		WriteTimeout: 0,
+		IdleTimeout:  0,
+	}
+	// config, _ := server.LoadConfigFile(os.Args[1])
+	// srv, _ := server.New(config)
+	// srv.Start()
 
-	log.Fatal(http.ListenAndServe(":8090", nil))
+	log.Fatal(server.ListenAndServe())
 }
server.go
@@ -1,17 +0,0 @@
-package main
-
-import (
-	"fmt"
-	"net/http"
-)
-
-type Server struct {
-}
-
-func NewServer() Server {
-	return Server{}
-}
-
-func (s Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
-	fmt.Fprintf(w, "OK")
-}
server_test.go
@@ -1,22 +0,0 @@
-package main
-
-import (
-	"net/http"
-	"net/http/httptest"
-	"testing"
-
-	"github.com/stretchr/testify/assert"
-)
-
-func TestServer(t *testing.T) {
-	t.Run("GET /health", func(t *testing.T) {
-		response := httptest.NewRecorder()
-		request, _ := http.NewRequest("GET", "/health", nil)
-
-		server := NewServer()
-		server.ServeHTTP(response, request)
-
-		assert.Equal(t, http.StatusOK, response.Code)
-		assert.Equal(t, "OK", response.Body.String())
-	})
-}