diff --git a/go.mod b/go.mod index 218c3c2..8248a3f 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/echo-webkom/goat -go 1.21.1 +go 1.22.1 require github.com/markbates/goth v1.79.0 diff --git a/internal/auth/sample/sample.go b/internal/auth/sample/sample.go new file mode 100644 index 0000000..0b5d5c9 --- /dev/null +++ b/internal/auth/sample/sample.go @@ -0,0 +1,125 @@ +package sample + +import ( + "context" + "encoding/json" + "io" + "log" + "net/http" + + "golang.org/x/oauth2" +) + +func resJson(w http.ResponseWriter, j any) { + b, err := json.Marshal(j) + if err != nil { + log.Println("JSON marshal error: " + err.Error()) + w.WriteHeader(http.StatusInternalServerError) + return + } + + w.Header().Add("Content-Type", "application/json") + w.Write(b) +} + +// Mount endpoints handled by provider for testing +func mountExampleHandlers(s *http.ServeMux) { + // Example login page, will be replaced with provider URL + s.HandleFunc("GET /sample/auth", func(w http.ResponseWriter, r *http.Request) { + http.ServeFile(w, r, "internal/auth/sample/sample_auth.html") + }) + + // Used for token exchange + s.HandleFunc("POST /sample/tokenUrl", func(w http.ResponseWriter, r *http.Request) { + resJson(w, map[string]any{ + "access_token": "abcdef", + "token_type": "bearer", + "expires_in": 3600, + "refresh_token": "ghijklmno", + "scope": "", + }) + }) + + // Used to fetch user data with generated token + s.HandleFunc("GET /sample/tokenUrl", func(w http.ResponseWriter, r *http.Request) { + resJson(w, map[string]any{ + "username": "bob", + "access_token": r.URL.Query().Get("access_token"), + }) + }) +} + +// Todo: create generic newProvider function + +func New(s *http.ServeMux) { + mountExampleHandlers(s) + + const ( + // Load from .env + CLIENT_ID = "john" + CLIENT_SECRET = "1234" + + AUTH_URL = "http://localhost:8080/sample/auth" + TOKEN_URL = "http://localhost:8080/sample/tokenUrl" + ) + + config := oauth2.Config{ + RedirectURL: "http://localhost:8080/sample_callback", + ClientID: CLIENT_ID, + ClientSecret: CLIENT_SECRET, + Scopes: []string{}, + Endpoint: oauth2.Endpoint{ + AuthURL: AUTH_URL, + TokenURL: TOKEN_URL, + }, + } + + s.Handle("GET /sample_login", login(config)) + s.Handle("POST /sample_callback", callback(config)) +} + +// Creates new login handler. Should redirect to providers auth URL with +// generated state. URL is given client id/secret, redirect uri, callback +// uri and state. +func login(config oauth2.Config) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + url := config.AuthCodeURL("randomstate") + http.Redirect(w, r, url, http.StatusSeeOther) + } +} + +// Creates a new callback handler for the auth provider. Verifies state +// and creates access token from code given by provider. This handler simply +// responds with the user data json. +func callback(config oauth2.Config) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + state := r.FormValue("state") + if state != "randomstate" { + w.WriteHeader(http.StatusInternalServerError) + return + } + + code := r.FormValue("code") + + token, err := config.Exchange(context.Background(), code) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + return + } + + token_url := config.Endpoint.TokenURL + "?access_token=" + token.AccessToken + resp, err := http.Get(token_url) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + return + } + + userData, err := io.ReadAll(resp.Body) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + return + } + + w.Write(userData) + } +} diff --git a/internal/auth/sample/sample_auth.html b/internal/auth/sample/sample_auth.html new file mode 100644 index 0000000..30f9b6e --- /dev/null +++ b/internal/auth/sample/sample_auth.html @@ -0,0 +1,19 @@ + + +
+ + +hello auth
+ + + + \ No newline at end of file diff --git a/internal/server/handler.go b/internal/server/handler.go index fee3008..7d6841d 100644 --- a/internal/server/handler.go +++ b/internal/server/handler.go @@ -2,24 +2,29 @@ package server import "net/http" -type HandlerWithCtx struct { - res http.ResponseWriter - req *http.Request +type Context struct { + res http.ResponseWriter + req *http.Request + name string } -type HandlerFunc func(HandlerWithCtx) +type HandlerFunc func(Context) func NewHandler(f HandlerFunc) HandlerFunc { return f } -func ToHttpHandler(f HandlerFunc) http.HandlerFunc { +func ToHttpHandlerFunc(f HandlerFunc) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { - f(HandlerWithCtx{w, r, ""}) // Create empty base context + f(Context{w, r, ""}) // Create empty base context } } +func ToHttpHandler(f HandlerFunc) http.Handler { + return ToHttpHandlerFunc(f) +} + type Middleware func(HandlerFunc) HandlerFunc func NewMiddleware(m Middleware) Middleware { diff --git a/internal/server/server.go b/internal/server/server.go index 169811a..8e0ad53 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -3,22 +3,22 @@ package server import ( "net/http" - "github.com/go-chi/chi/v5" + "github.com/echo-webkom/goat/internal/auth/sample" ) -type Config struct { - Addr string +type Server struct { + Router *http.ServeMux + Config Config } -type Server struct { - Router *chi.Mux - Config *Config +type Config struct { + Addr string } func New() *Server { - r := chi.NewRouter() + r := http.NewServeMux() - cfg := &Config{ + cfg := Config{ Addr: ":8080", } @@ -35,17 +35,20 @@ func (s *Server) Run(addr string) error { func (s *Server) MountHandlers() { // Create simple base handler using a context - handler := NewHandler(func(hwc HandlerWithCtx) { - hwc.res.Write([]byte("Hello " + hwc.name)) + handler := NewHandler(func(ctx Context) { + ctx.res.Write([]byte("Hello " + ctx.name)) }) - // Create middleware that writes to the context before calling the handler + // Create myMiddleware that writes to the context before calling the handler middleware := NewMiddleware(func(hf HandlerFunc) HandlerFunc { - return func(hwc HandlerWithCtx) { - hwc.name = "John" - hf(hwc) + return func(ctx Context) { + ctx.name = "John" + hf(ctx) } }) - s.Router.Get("/", ToHttpHandler(middleware(handler))) + s.Router.Handle("GET /test", ToHttpHandlerFunc(middleware(handler))) + + // Sample oauth2 flow, go to /sample_login + sample.New(s.Router) }