diff options
| author | mo khan <mo@mokhan.ca> | 2022-04-20 17:37:42 -0600 |
|---|---|---|
| committer | mo khan <mo@mokhan.ca> | 2022-04-20 17:37:42 -0600 |
| commit | ed7a6333ed475b84f64dcf22e7318297867348d9 (patch) | |
| tree | d4b4ad13e5070966fa1549fbebaeb885920d70d9 | |
| parent | 30fa85a7c2c482c0abe8fa0b8275d4766ac85aa0 (diff) | |
build a tiny oidc provider
| -rw-r--r-- | go.mod | 20 | ||||
| -rw-r--r-- | go.sum | 31 | ||||
| -rw-r--r-- | main.go | 173 | ||||
| -rw-r--r-- | server.go | 17 | ||||
| -rw-r--r-- | server_test.go | 22 |
5 files changed, 213 insertions, 50 deletions
@@ -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 ) @@ -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= @@ -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()) } diff --git a/server.go b/server.go deleted file mode 100644 index e69f35b..0000000 --- a/server.go +++ /dev/null @@ -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") -} diff --git a/server_test.go b/server_test.go deleted file mode 100644 index fc878f9..0000000 --- a/server_test.go +++ /dev/null @@ -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()) - }) -} |
