From 7ec7e5596580510607e926a7d08b0c032f149b44 Mon Sep 17 00:00:00 2001 From: jesperkha Date: Tue, 23 Apr 2024 13:47:24 +0200 Subject: [PATCH] generic callback and login handlers --- internal/auth/auth.go | 109 ++++++++++++++++++++++++++ internal/auth/sample/sample.go | 91 +++------------------ internal/auth/sample/sample_auth.html | 6 +- internal/server/server.go | 16 +++- 4 files changed, 137 insertions(+), 85 deletions(-) create mode 100644 internal/auth/auth.go diff --git a/internal/auth/auth.go b/internal/auth/auth.go new file mode 100644 index 0000000..3308788 --- /dev/null +++ b/internal/auth/auth.go @@ -0,0 +1,109 @@ +package auth + +import ( + "context" + "encoding/json" + "io" + "log" + "net/http" + + "golang.org/x/oauth2" +) + +type Provider struct { + config oauth2.Config +} + +func New(clientId, clientSecret, authUrl, tokenUrl string) Provider { + config := oauth2.Config{ + // Default auth callback for testing. Remove + RedirectURL: "http://localhost:8080/auth/{provider}/callback", + ClientID: clientId, + ClientSecret: clientSecret, + Scopes: []string{}, + Endpoint: oauth2.Endpoint{ + AuthURL: authUrl, + TokenURL: tokenUrl, + }, + } + + return Provider{config: config} +} + +type User struct { + AccessToken string `json:"access_token"` + TokenType string `json:"token_type"` + ExpiresIn int `json:"expires_in"` + RefreshToken string `json:"refresh_token"` + Scope string `json:"scope"` + Error string `json:"error"` + ErrorDesc string `json:"error_description"` + ErrorUri string `json:"error_uri"` +} + +type Session struct { + User User + Writer http.ResponseWriter + Request *http.Request +} + +// LoginHandler return a http.HandlerFunc used to handle the endpoint /auth/{provider} +func LoginHandler(providers map[string]Provider) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + providerName := r.PathValue("provider") + if p, ok := providers[providerName]; ok { + url := p.config.AuthCodeURL("randomstate") + http.Redirect(w, r, url, http.StatusSeeOther) + } else { + log.Println("login: provider not found: ", providerName) + w.WriteHeader(http.StatusNotFound) + } + } +} + +// CallbackHandler returns a http.HandlerFunc used to handle the endpoint /auth/{provider}/callback +func CallbackHandler(providers map[string]Provider, userCallback func(Session)) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + providerName := r.PathValue("provider") + + if p, ok := providers[providerName]; ok { + userData, err := p.fetchUserData(r) + if err != nil { + log.Println(err) + return + } + + var user User + if err = json.Unmarshal(userData, &user); err != nil { + log.Println(err) + return + } + + userCallback(Session{ + User: user, + Request: r, + Writer: w, + }) + } else { + log.Println("callback: provider not found: ", providerName) + w.WriteHeader(http.StatusNotFound) + } + } +} + +func (p *Provider) fetchUserData(r *http.Request) ([]byte, error) { + code := r.FormValue("code") + + token, err := p.config.Exchange(context.Background(), code) + if err != nil { + return nil, err + } + + token_url := p.config.Endpoint.TokenURL + "?access_token=" + token.AccessToken + resp, err := http.Get(token_url) + if err != nil { + return nil, err + } + + return io.ReadAll(resp.Body) +} diff --git a/internal/auth/sample/sample.go b/internal/auth/sample/sample.go index 0b5d5c9..211403d 100644 --- a/internal/auth/sample/sample.go +++ b/internal/auth/sample/sample.go @@ -1,13 +1,11 @@ package sample import ( - "context" "encoding/json" - "io" "log" "net/http" - "golang.org/x/oauth2" + "github.com/echo-webkom/goat/internal/auth" ) func resJson(w http.ResponseWriter, j any) { @@ -23,7 +21,7 @@ func resJson(w http.ResponseWriter, j any) { } // Mount endpoints handled by provider for testing -func mountExampleHandlers(s *http.ServeMux) { +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") @@ -32,94 +30,27 @@ func mountExampleHandlers(s *http.ServeMux) { // Used for token exchange s.HandleFunc("POST /sample/tokenUrl", func(w http.ResponseWriter, r *http.Request) { resJson(w, map[string]any{ - "access_token": "abcdef", + "access_token": "VeryCoolAccessToken", "token_type": "bearer", "expires_in": 3600, - "refresh_token": "ghijklmno", - "scope": "", + "refresh_token": "CoolerRefreshToken", + "scope": "CoolSCope", }) }) // 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" +func New() auth.Provider { + return auth.New( + "cooluserid", + "coolusersecret", + "http://localhost:8080/sample/auth", + "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 index 30f9b6e..c9f7763 100644 --- a/internal/auth/sample/sample_auth.html +++ b/internal/auth/sample/sample_auth.html @@ -6,11 +6,11 @@ Document -

hello auth

+

Very cool auth provider login screen thing.com

-
+
-
+



diff --git a/internal/server/server.go b/internal/server/server.go index 8e0ad53..a13a9d4 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -1,8 +1,10 @@ package server import ( + "encoding/json" "net/http" + "github.com/echo-webkom/goat/internal/auth" "github.com/echo-webkom/goat/internal/auth/sample" ) @@ -49,6 +51,16 @@ func (s *Server) MountHandlers() { s.Router.Handle("GET /test", ToHttpHandlerFunc(middleware(handler))) - // Sample oauth2 flow, go to /sample_login - sample.New(s.Router) + // Sample oauth2 flow, go to /auth/sample + ps := map[string]auth.Provider{ + "sample": sample.New(), + } + + s.Router.HandleFunc("/auth/{provider}", auth.LoginHandler(ps)) + s.Router.HandleFunc("/auth/{provider}/callback", auth.CallbackHandler(ps, func(s auth.Session) { + d, _ := json.Marshal(s.User) + s.Writer.Write(d) + })) + + sample.MountExampleHandlers(s.Router) }