Commit c84a523

mo khan <mo@mokhan.ca>
2022-04-21 16:21:35
create a minimal oidc idp
1 parent ed7a633
cmd/server/main.go
@@ -0,0 +1,20 @@
+package main
+
+import (
+	"log"
+	"net/http"
+
+	"mokhan.ca/xlgmokha/oauth/pkg/web"
+)
+
+func main() {
+	log.Println("Starting server, listening on port 8282.")
+	server := &http.Server{
+		Addr:         ":8282",
+		Handler:      web.NewHandler(),
+		ReadTimeout:  0,
+		WriteTimeout: 0,
+		IdleTimeout:  0,
+	}
+	log.Fatal(server.ListenAndServe())
+}
pkg/web/authorize.go
@@ -0,0 +1,53 @@
+package web
+
+import (
+	"fmt"
+	"net/http"
+
+	"github.com/hashicorp/uuid"
+)
+
+type AuthorizationRequest struct {
+	ResponseType string
+	Scope        string
+	ClientId     string
+	State        string
+	RedirectUri  string
+	Nonce        string
+}
+
+func (h *HttpContext) Authorize(w http.ResponseWriter, r *http.Request) {
+	if 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 := h.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")
+		}
+	}
+}
pkg/web/default.go
@@ -0,0 +1,7 @@
+package web
+
+import "net/http"
+
+func (h *HttpContext) Default(w http.ResponseWriter, r *http.Request) {
+	w.WriteHeader(http.StatusOK)
+}
pkg/web/http_mux.go
@@ -0,0 +1,66 @@
+package web
+
+import (
+	"io/ioutil"
+	"log"
+	"net/http"
+	"os"
+	"time"
+
+	"github.com/golang-jwt/jwt"
+	"github.com/hashicorp/uuid"
+)
+
+var (
+	tokens = map[string]string{}
+)
+
+type IdTokenFactory func(clientId string) string
+
+func (h *HttpContext) 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(),
+	})
+
+	key, _ := jwt.ParseRSAPrivateKeyFromPEM(h.keyData)
+	signedIdToken, _ := idToken.SignedString(key)
+	return signedIdToken
+}
+
+type HttpContext struct {
+	log     *log.Logger
+	keyData []byte
+}
+
+func NewHandler() http.Handler {
+	keyData, _ := ioutil.ReadFile("insecure.pem")
+	h := &HttpContext{
+		log:     log.Default(),
+		keyData: keyData,
+	}
+	mux := http.NewServeMux()
+	mux.Handle("/", http.HandlerFunc(h.Default))
+	mux.Handle("/.well-known/jwks.json", http.HandlerFunc(h.WellKnown))
+	mux.Handle("/.well-known/openid-configuration", http.HandlerFunc(h.WellKnown))
+	mux.Handle("/authorize", http.HandlerFunc(h.Authorize))
+	mux.Handle("/revoke", http.HandlerFunc(http.NotFound))
+	mux.Handle("/token", http.HandlerFunc(h.Token))
+	mux.Handle("/userinfo", http.HandlerFunc(http.NotFound))
+	return mux
+}
pkg/web/token.go
@@ -0,0 +1,48 @@
+package web
+
+import (
+	"fmt"
+	"net/http"
+)
+
+type TokenRequest struct {
+	GrantType   string
+	Code        string
+	RedirectUri string
+}
+
+type TokenResponse struct {
+	AccessToken  string
+	TokenType    string
+	RefreshToken string
+	ExpiresIn    int
+	IdToken      string
+}
+
+func (h *HttpContext) Token(w http.ResponseWriter, r *http.Request) {
+	if 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:      h.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")
+		}
+	}
+}
pkg/web/well_known.go
@@ -0,0 +1,40 @@
+package web
+
+import (
+	"crypto/x509"
+	"encoding/json"
+	"encoding/pem"
+	"fmt"
+	"io/ioutil"
+	"net/http"
+	"os"
+	"text/template"
+
+	"github.com/lestrrat-go/jwx/v2/jwk"
+)
+
+func (h *HttpContext) WellKnown(w http.ResponseWriter, r *http.Request) {
+	fmt.Println(r.URL.Path)
+	if r.URL.Path == "/.well-known/openid-configuration" {
+		w.Header().Set("Content-Type", "application/json")
+		data, _ := ioutil.ReadFile("./public/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 == "/.well-known/jwks.json" {
+		w.Header().Set("Content-Type", "application/json")
+		privatePem, _ := pem.Decode(h.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)
+	}
+}
public/openid-configuration.json
@@ -0,0 +1,37 @@
+{
+  "issuer": "{{.Host}}",
+  "authorization_endpoint": "{{.Host}}/authorize",
+  "token_endpoint": "{{.Host}}/token",
+  "userinfo_endpoint": "{{.Host}}/userinfo",
+  "jwks_uri": "{{.Host}}/.well-known/jwks.json",
+  "revocation_endpoint": "{{.Host}}/revoke",
+  "scopes_supported": [
+    "openid"
+  ],
+  "response_types_supported": [
+    "code id_token token",
+    "code id_token",
+    "code token",
+    "code",
+    "id_token token",
+    "id_token"
+  ],
+  "response_modes_supported": [
+    "query",
+    "fragment",
+    "form_post"
+  ],
+  "subject_types_supported": [
+    "public"
+  ],
+  "id_token_signing_alg_values_supported": [
+    "RS256"
+  ],
+  "claims_supported": [
+    "aud",
+    "exp",
+    "iat",
+    "iss",
+    "sub"
+  ]
+}
insecure.pem
@@ -0,0 +1,51 @@
+-----BEGIN RSA PRIVATE KEY-----
+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
+-----END RSA PRIVATE KEY-----
main.go
@@ -1,181 +0,0 @@
-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"
-)
-
-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() {
-	log.Println("Starting server, listening on port 8282.")
-
-	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(server.ListenAndServe())
-}