feat(oauth,twitter): default oauth2 implementation
This commit is contained in:
24
internals/app/oauth.go
Normal file
24
internals/app/oauth.go
Normal file
@@ -0,0 +1,24 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"extrovert/internals/oauth"
|
||||
"net/url"
|
||||
"os"
|
||||
)
|
||||
|
||||
const DOMAIN = "http://localhost:7331/"
|
||||
|
||||
const TWITTER_REDIRECT = "/api/twitter/oauth2"
|
||||
|
||||
var TWITTER_APP = func() oauth.TwitterOAuth {
|
||||
ru, _ := url.Parse(DOMAIN)
|
||||
ru = ru.JoinPath(TWITTER_REDIRECT)
|
||||
|
||||
c := oauth.NewTwitterOAuth(
|
||||
os.Getenv("TWITTER_CLIENT_ID"),
|
||||
os.Getenv("TWITTER_CLIENT_SECRET"),
|
||||
ru,
|
||||
)
|
||||
|
||||
return c
|
||||
}()
|
||||
@@ -1,90 +0,0 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"extrovert/templates/pages"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"github.com/a-h/templ"
|
||||
)
|
||||
|
||||
type Client interface {
|
||||
OAuthHandler(w http.ResponseWriter, r *http.Request)
|
||||
}
|
||||
|
||||
type DefaultClient struct {
|
||||
name string
|
||||
tokenEndpoint url.URL
|
||||
id string
|
||||
redirectUri string
|
||||
}
|
||||
|
||||
func (c DefaultClient) OAuthHandler(w http.ResponseWriter, r *http.Request) {
|
||||
code := r.URL.Query().Get("code")
|
||||
if code == "" {
|
||||
log.Fatalf("TODO-ERR missing code parameter")
|
||||
}
|
||||
|
||||
req := c.tokenEndpoint
|
||||
|
||||
q := req.Query()
|
||||
q.Add("grant_type", "authorization_code")
|
||||
q.Add("code_verifier", "challenge")
|
||||
q.Add("challenge_method", "plain")
|
||||
q.Add("code", code)
|
||||
q.Add("client_id", c.id)
|
||||
q.Add("redirect_uri", c.redirectUri)
|
||||
|
||||
res, err := http.Post(req.String(), "application/x-www-form-urlencoded", bytes.NewReader([]byte("")))
|
||||
if err != nil {
|
||||
log.Fatalf("TODO-ERR trying to get token on %s, error:\n%s", req.Host, err)
|
||||
}
|
||||
|
||||
body, err := io.ReadAll(res.Body)
|
||||
if err != nil || res.StatusCode != 200 {
|
||||
log.Fatalf("TODO-ERR trying to read body on %s, body:\n%s\n\nerror:\n%s", req.Host, body, err)
|
||||
}
|
||||
|
||||
var token DefaultClientToken
|
||||
err = json.Unmarshal(body, &token)
|
||||
if err != nil {
|
||||
log.Fatalf("TODO-ERR trying to parse json body to token:\n%s", err)
|
||||
}
|
||||
|
||||
cookie := http.Cookie{
|
||||
Name: strings.ToUpper("__Host-TOKEN-" + c.name),
|
||||
// Value: token.String(),
|
||||
SameSite: http.SameSiteStrictMode,
|
||||
Path: "/",
|
||||
Secure: true,
|
||||
}
|
||||
|
||||
http.SetCookie(w, &cookie)
|
||||
|
||||
err = pages.RedirectPopUp(
|
||||
"Logged into "+c.name+"!",
|
||||
"Your "+c.name+" account was succeffully logged into project-extrovert. ",
|
||||
templ.SafeURL("/index.html"),
|
||||
).Render(context.Background(), w)
|
||||
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
log.Fatalf("TODO-ERR trying to render static page:\n%s", err)
|
||||
return
|
||||
}
|
||||
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}
|
||||
|
||||
type DefaultClientToken struct {
|
||||
Type string `json:"token_type"`
|
||||
Token string `json:"access_token"`
|
||||
ExpiresIn int `json:"expires_in"`
|
||||
Scope string `json:"scope"`
|
||||
}
|
||||
148
internals/oauth/oauth.templ
Normal file
148
internals/oauth/oauth.templ
Normal file
@@ -0,0 +1,148 @@
|
||||
package oauth
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
e "errors"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"fmt"
|
||||
|
||||
"extrovert/internals/router/errors"
|
||||
"extrovert/templates/pages"
|
||||
)
|
||||
|
||||
type OAuthClient interface {
|
||||
ServeHTTP(w http.ResponseWriter, r *http.Request)
|
||||
LoginButton() templ.Component
|
||||
}
|
||||
|
||||
type DefaultOAuthToken struct {
|
||||
Type string `json:"token_type"`
|
||||
Token string `json:"access_token"`
|
||||
Expires int `json:"expires_in"`
|
||||
Scope string `json:"scope"`
|
||||
RefreshToken *string `json:"refresh_token"`
|
||||
}
|
||||
|
||||
type DefaultOAuthClient struct {
|
||||
Name string
|
||||
Id string
|
||||
Secret string
|
||||
AuthEndpoint *url.URL
|
||||
TokenEndpoint *url.URL
|
||||
RedirectUri *url.URL
|
||||
}
|
||||
|
||||
func NewDefaultOAuthClient(u *url.URL, id string, secret string, redirect *url.URL) DefaultOAuthClient {
|
||||
auth, _ := url.Parse(u.String())
|
||||
|
||||
q := auth.Query()
|
||||
q.Add("client_id", id)
|
||||
q.Add("redirect_uri", redirect.String())
|
||||
q.Add("response_type", "code")
|
||||
q.Add("scope", "read write")
|
||||
q.Add("state", "state")
|
||||
q.Add("code_challenge", "challenge")
|
||||
q.Add("code_challenge_method", "plain")
|
||||
auth.RawQuery = q.Encode()
|
||||
|
||||
auth = auth.JoinPath("/oauth2/authorize")
|
||||
|
||||
token, _ := url.Parse(u.String())
|
||||
token = token.JoinPath("/oauth2/token")
|
||||
|
||||
return DefaultOAuthClient{
|
||||
Name: u.Hostname(),
|
||||
Id: id,
|
||||
Secret: secret,
|
||||
AuthEndpoint: auth,
|
||||
TokenEndpoint: token,
|
||||
RedirectUri: redirect,
|
||||
}
|
||||
}
|
||||
|
||||
templ (c DefaultOAuthClient) LoginButton() {
|
||||
<a
|
||||
role="button"
|
||||
href={ templ.SafeURL(c.AuthEndpoint.String()) }
|
||||
rel="noopener noreferrer nofollow"
|
||||
>
|
||||
Login on { c.Name }
|
||||
</a>
|
||||
}
|
||||
|
||||
func (c DefaultOAuthClient) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
code := r.URL.Query().Get("code")
|
||||
if code == "" {
|
||||
errors.NewErrMissingParams("code").ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
req := c.TokenEndpoint
|
||||
|
||||
q := req.Query()
|
||||
q.Add("client_id", c.Id)
|
||||
q.Add("client_secret", c.Secret)
|
||||
q.Add("code", code)
|
||||
q.Add("redirect_uri", c.RedirectUri.String())
|
||||
q.Add("grant_type", "authorization_code")
|
||||
q.Add("code_verifier", "challenge")
|
||||
q.Add("challenge_method", "plain")
|
||||
req.RawQuery = q.Encode()
|
||||
|
||||
res, err := http.Post(req.String(), "application/x-www-form-urlencoded", bytes.NewReader([]byte("")))
|
||||
if err != nil {
|
||||
errors.NewErrInternal(e.New("Error while trying to request token"), err).ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
body, err := io.ReadAll(res.Body)
|
||||
if err != nil {
|
||||
errors.NewErrInternal(e.New("Error while trying to read token response body"), err).ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
if res.StatusCode != 200 {
|
||||
errors.NewErrInternal(fmt.Errorf(
|
||||
"Unable to retrieve token, non-200 response from platform. \nStatus: %v\nBody: %s",
|
||||
res.StatusCode, string(body)),
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
var token DefaultOAuthToken
|
||||
err = json.Unmarshal(body, &token)
|
||||
if err != nil {
|
||||
errors.NewErrInternal(e.New("Error while trying to validate token response body"), err).ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
cv, err := json.Marshal(token)
|
||||
if err != nil {
|
||||
errors.NewErrInternal(e.New("Error while trying to validate token response body"), err).ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
http.SetCookie(w, &http.Cookie{
|
||||
Name: "__Host-OAUTH-" + strings.ToUpper(c.Name),
|
||||
Value: string(cv),
|
||||
SameSite: http.SameSiteStrictMode,
|
||||
Path: "/",
|
||||
Secure: true,
|
||||
HttpOnly: true,
|
||||
})
|
||||
|
||||
err = pages.RedirectPopUp(
|
||||
"Logged into "+c.Name+"!",
|
||||
"Your "+c.Name+" account was successfully logged into project-extrovert. "+
|
||||
"Click the button below to return to the Homepage.",
|
||||
templ.SafeURL("/"),
|
||||
).Render(r.Context(), w)
|
||||
|
||||
if err != nil {
|
||||
errors.NewErrInternal(e.New("Unable to render redirect page"), err).ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}
|
||||
23
internals/oauth/twitter.go
Normal file
23
internals/oauth/twitter.go
Normal file
@@ -0,0 +1,23 @@
|
||||
package oauth
|
||||
|
||||
import "net/url"
|
||||
|
||||
type TwitterOAuth struct {
|
||||
DefaultOAuthClient
|
||||
}
|
||||
|
||||
func NewTwitterOAuth(id string, secret string, redirect *url.URL) TwitterOAuth {
|
||||
u, _ := url.Parse("https://api.twitter.com/2/")
|
||||
c := NewDefaultOAuthClient(u, id, secret, redirect)
|
||||
|
||||
c.Name = "Twitter"
|
||||
|
||||
u, _ = url.Parse("https://twitter.com/i/oauth2/authorize")
|
||||
q := c.AuthEndpoint.Query()
|
||||
q.Del("scope")
|
||||
q.Add("scope", "tweet.read tweet.write users.read")
|
||||
u.RawQuery = q.Encode()
|
||||
c.AuthEndpoint = u
|
||||
|
||||
return TwitterOAuth{c}
|
||||
}
|
||||
2
main.go
2
main.go
@@ -28,7 +28,7 @@ func main() {
|
||||
if *dev {
|
||||
r.AddMiddleware(middlewares.NewDevelopmentMiddleware(logger))
|
||||
}
|
||||
r.AddMiddleware(middlewares.NewCookiesCryptoMiddleware(os.Getenv("CRYPTO_COOKIES_KEY"), logger))
|
||||
r.AddMiddleware(middlewares.NewCookiesCryptoMiddleware(os.Getenv("CRYPTO_COOKIE_KEY"), logger))
|
||||
|
||||
err := http.ListenAndServe(fmt.Sprintf(":%v", *port), r)
|
||||
if err != nil {
|
||||
|
||||
@@ -3,6 +3,7 @@ package routes
|
||||
import (
|
||||
"extrovert/templates/layouts"
|
||||
"extrovert/components"
|
||||
"extrovert/internals/app"
|
||||
"net/http"
|
||||
"extrovert/internals/router/errors"
|
||||
e "errors"
|
||||
@@ -12,6 +13,7 @@ type Homepage struct{}
|
||||
|
||||
func (h Homepage) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
|
||||
err := h.page().Render(context.Background(), w)
|
||||
if err != nil {
|
||||
errors.NewErrInternal(e.New("Unable to render dashboard"), err).ServeHTTP(w, r)
|
||||
@@ -29,8 +31,7 @@ templ (h Homepage) page() {
|
||||
style="height:100%;display:flex;gap:2rem;"
|
||||
>
|
||||
<div style="display:flex;flex-direction:column;gap:1rem;width:15rem;">
|
||||
@components.LoginTwitter()
|
||||
@components.LoginMastodon()
|
||||
@app.TWITTER_APP.LoginButton()
|
||||
</div>
|
||||
<fieldset>
|
||||
<textarea
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package routes
|
||||
|
||||
import (
|
||||
"extrovert/internals/app"
|
||||
"extrovert/internals/router"
|
||||
)
|
||||
|
||||
@@ -8,4 +9,5 @@ var ROUTES = []router.Route{
|
||||
{Pattern: "/{$}", Handler: Homepage{}},
|
||||
{Pattern: "/robots.txt", Handler: RobotsTxt{}},
|
||||
{Pattern: "/ai.txt", Handler: AITxt{}},
|
||||
{Pattern: app.TWITTER_REDIRECT, Handler: app.TWITTER_APP},
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user