mirror of
https://github.com/mentos1386/zdravko.git
synced 2024-11-21 23:33:34 +00:00
feat: fully implemented oauth2 authentication and cookie sessions
This commit is contained in:
parent
130a068a7d
commit
0a323c79e6
11 changed files with 209 additions and 5 deletions
|
@ -44,11 +44,14 @@ func main() {
|
|||
r.PathPrefix("/static/").Handler(http.StripPrefix("/static/", http.FileServer(http.FS(static.Static))))
|
||||
|
||||
r.HandleFunc("/", h.Index).Methods("GET")
|
||||
r.HandleFunc("/settings", h.Settings).Methods("GET")
|
||||
|
||||
// Authenticated routes
|
||||
r.HandleFunc("/settings", h.Authenticated(h.Settings)).Methods("GET")
|
||||
|
||||
// OAuth2
|
||||
r.HandleFunc("/oauth2/login", h.OAuth2LoginGET).Methods("GET")
|
||||
r.HandleFunc("/oauth2/callback", h.OAuth2CallbackGET).Methods("GET")
|
||||
r.HandleFunc("/oauth2/logout", h.Authenticated(h.OAuth2LogoutGET)).Methods("GET")
|
||||
|
||||
log.Println("Server started on", config.PORT)
|
||||
log.Fatal(http.ListenAndServe(":"+config.PORT, r))
|
||||
|
|
|
@ -16,3 +16,4 @@ OAUTH2_SCOPES=openid,profile,email
|
|||
OAUTH2_ENDPOINT_TOKEN_URL=https://your_oauth2_provider/token
|
||||
OAUTH2_ENDPOINT_AUTH_URL=https://your_oauth2_provider/auth
|
||||
OAUTH2_ENDPOINT_USER_INFO_URL=https://your_oauth2_provider/userinfo
|
||||
OAUTH2_ENDPOINT_USER_INFO_URL=https://your_oauth2_provider/logout
|
||||
|
|
1
fly.toml
1
fly.toml
|
@ -16,6 +16,7 @@ primary_region = 'waw'
|
|||
OAUTH2_ENDPOINT_TOKEN_URL = 'https://id.tjo.space/application/o/token/'
|
||||
OAUTH2_ENDPOINT_AUTH_URL = 'https://id.tjo.space/application/o/authorize/'
|
||||
OAUTH2_ENDPOINT_USER_INFO_URL = 'https://id.tjo.space/application/o/userinfo/'
|
||||
OAUTH2_ENDPOINT_LOGOUT_URL = 'https://id.tjo.space/application/o/zdravko-development/end-session/'
|
||||
|
||||
[processes]
|
||||
server = "server"
|
||||
|
|
2
go.mod
2
go.mod
|
@ -23,6 +23,8 @@ require (
|
|||
github.com/golang/mock v1.6.0 // indirect
|
||||
github.com/golang/protobuf v1.5.3 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/gorilla/securecookie v1.1.2 // indirect
|
||||
github.com/gorilla/sessions v1.2.2 // indirect
|
||||
github.com/grpc-ecosystem/go-grpc-middleware v1.3.0 // indirect
|
||||
github.com/grpc-ecosystem/grpc-gateway v1.16.0 // indirect
|
||||
github.com/jinzhu/inflection v1.0.0 // indirect
|
||||
|
|
4
go.sum
4
go.sum
|
@ -972,6 +972,10 @@ github.com/googleapis/go-type-adapters v1.0.0/go.mod h1:zHW75FOG2aur7gAO2B+MLby+
|
|||
github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g=
|
||||
github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
|
||||
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
|
||||
github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA=
|
||||
github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo=
|
||||
github.com/gorilla/sessions v1.2.2 h1:lqzMYz6bOfvn2WriPUjNByzeXIlVzURcPmgMczkmTjY=
|
||||
github.com/gorilla/sessions v1.2.2/go.mod h1:ePLdVu+jbEgHH+KWw8I1z2wqd0BAdAQh/8LRvBeoNcQ=
|
||||
github.com/grpc-ecosystem/go-grpc-middleware v1.3.0 h1:+9834+KizmvFV7pXQGSXQTsaWhq2GjuNUt0aUU0YBYw=
|
||||
github.com/grpc-ecosystem/go-grpc-middleware v1.3.0/go.mod h1:z0ButlSOZa5vEBq9m2m2hlwIgKw+rp3sdCBRoJY+30Y=
|
||||
github.com/grpc-ecosystem/grpc-gateway v1.16.0 h1:gmcG1KaJ57LophUzW0Hy8NmPhnMZb4M0+kPpLofRdBo=
|
||||
|
|
|
@ -19,6 +19,7 @@ type Config struct {
|
|||
OAUTH2_ENDPOINT_TOKEN_URL string
|
||||
OAUTH2_ENDPOINT_AUTH_URL string
|
||||
OAUTH2_ENDPOINT_USER_INFO_URL string
|
||||
OAUTH2_ENDPOINT_LOGOUT_URL string
|
||||
}
|
||||
|
||||
func getEnv(key, fallback string) string {
|
||||
|
@ -49,5 +50,6 @@ func NewConfig() *Config {
|
|||
OAUTH2_ENDPOINT_TOKEN_URL: getEnvRequired("OAUTH2_ENDPOINT_TOKEN_URL"),
|
||||
OAUTH2_ENDPOINT_AUTH_URL: getEnvRequired("OAUTH2_ENDPOINT_AUTH_URL"),
|
||||
OAUTH2_ENDPOINT_USER_INFO_URL: getEnvRequired("OAUTH2_ENDPOINT_USER_INFO_URL"),
|
||||
OAUTH2_ENDPOINT_LOGOUT_URL: getEnvRequired("OAUTH2_ENDPOINT_LOGOUT_URL"),
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,6 +3,7 @@ package handlers
|
|||
import (
|
||||
"code.tjo.space/mentos1386/zdravko/internal"
|
||||
"code.tjo.space/mentos1386/zdravko/internal/models/query"
|
||||
"github.com/gorilla/sessions"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
|
@ -10,8 +11,12 @@ type BaseHandler struct {
|
|||
db *gorm.DB
|
||||
query *query.Query
|
||||
config *internal.Config
|
||||
|
||||
store *sessions.CookieStore
|
||||
}
|
||||
|
||||
func NewBaseHandler(db *gorm.DB, q *query.Query, config *internal.Config) *BaseHandler {
|
||||
return &BaseHandler{db, q, config}
|
||||
store := sessions.NewCookieStore([]byte(config.SESSION_SECRET))
|
||||
|
||||
return &BaseHandler{db, q, config, store}
|
||||
}
|
||||
|
|
|
@ -2,6 +2,7 @@ package handlers
|
|||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
|
||||
|
@ -9,6 +10,11 @@ import (
|
|||
"golang.org/x/oauth2"
|
||||
)
|
||||
|
||||
type UserInfo struct {
|
||||
Sub string `json:"sub"`
|
||||
Email string `json:"email"`
|
||||
}
|
||||
|
||||
func newOAuth2(config *internal.Config) *oauth2.Config {
|
||||
return &oauth2.Config{
|
||||
ClientID: config.OAUTH2_CLIENT_ID,
|
||||
|
@ -22,6 +28,40 @@ func newOAuth2(config *internal.Config) *oauth2.Config {
|
|||
}
|
||||
}
|
||||
|
||||
func (h *BaseHandler) AuthenticatedUserToOAuth2Token(user *AuthenticatedUser) *oauth2.Token {
|
||||
return &oauth2.Token{
|
||||
AccessToken: user.OAuth2AccessToken,
|
||||
TokenType: user.OAuth2TokenType,
|
||||
RefreshToken: user.OAuth2RefreshToken,
|
||||
Expiry: user.OAuth2Expiry,
|
||||
}
|
||||
}
|
||||
|
||||
func (h *BaseHandler) RefreshToken(w http.ResponseWriter, r *http.Request, user *AuthenticatedUser) (*AuthenticatedUser, error) {
|
||||
tok := h.AuthenticatedUserToOAuth2Token(user)
|
||||
conf := newOAuth2(h.config)
|
||||
refreshed, err := conf.TokenSource(context.Background(), tok).Token()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
refreshedUser := &AuthenticatedUser{
|
||||
ID: user.ID,
|
||||
Email: user.Email,
|
||||
OAuth2AccessToken: refreshed.AccessToken,
|
||||
OAuth2RefreshToken: refreshed.RefreshToken,
|
||||
OAuth2TokenType: refreshed.TokenType,
|
||||
OAuth2Expiry: refreshed.Expiry,
|
||||
}
|
||||
|
||||
err = h.SetAuthenticatedUserForRequest(w, r, refreshedUser)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return refreshedUser, nil
|
||||
}
|
||||
|
||||
func (h *BaseHandler) OAuth2LoginGET(w http.ResponseWriter, r *http.Request) {
|
||||
conf := newOAuth2(h.config)
|
||||
|
||||
|
@ -54,10 +94,41 @@ func (h *BaseHandler) OAuth2CallbackGET(w http.ResponseWriter, r *http.Request)
|
|||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
_, err = w.Write(body)
|
||||
var userInfo UserInfo
|
||||
err = json.Unmarshal(body, &userInfo)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
err = h.SetAuthenticatedUserForRequest(w, r, &AuthenticatedUser{
|
||||
ID: userInfo.Sub,
|
||||
Email: userInfo.Email,
|
||||
OAuth2AccessToken: tok.AccessToken,
|
||||
OAuth2RefreshToken: tok.RefreshToken,
|
||||
OAuth2TokenType: tok.TokenType,
|
||||
OAuth2Expiry: tok.Expiry,
|
||||
})
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
http.Redirect(w, r, "/settings", http.StatusTemporaryRedirect)
|
||||
}
|
||||
|
||||
func (h *BaseHandler) OAuth2LogoutGET(w http.ResponseWriter, r *http.Request, user *AuthenticatedUser) {
|
||||
tok := h.AuthenticatedUserToOAuth2Token(user)
|
||||
client := oauth2.NewClient(context.Background(), oauth2.StaticTokenSource(tok))
|
||||
_, err := client.Get(h.config.OAUTH2_ENDPOINT_USER_INFO_URL)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
err = h.ClearAuthenticatedUserForRequest(w, r)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
http.Redirect(w, r, "/", http.StatusTemporaryRedirect)
|
||||
}
|
||||
|
|
111
internal/handlers/session.go
Normal file
111
internal/handlers/session.go
Normal file
|
@ -0,0 +1,111 @@
|
|||
package handlers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
const sessionName = "zdravko-hey"
|
||||
|
||||
type AuthenticatedUser struct {
|
||||
ID string
|
||||
Email string
|
||||
OAuth2AccessToken string
|
||||
OAuth2RefreshToken string
|
||||
OAuth2TokenType string
|
||||
OAuth2Expiry time.Time
|
||||
}
|
||||
|
||||
type authenticatedUserKeyType string
|
||||
|
||||
const authenticatedUserKey authenticatedUserKeyType = "authenticatedUser"
|
||||
|
||||
func WithUser(ctx context.Context, user *AuthenticatedUser) context.Context {
|
||||
return context.WithValue(ctx, authenticatedUserKey, user)
|
||||
}
|
||||
|
||||
func GetUser(ctx context.Context) *AuthenticatedUser {
|
||||
user, ok := ctx.Value(authenticatedUserKey).(*AuthenticatedUser)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
return user
|
||||
}
|
||||
|
||||
func (h *BaseHandler) GetAuthenticatedUserForRequest(r *http.Request) (*AuthenticatedUser, error) {
|
||||
session, err := h.store.Get(r, sessionName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if session.IsNew {
|
||||
return nil, fmt.Errorf("session is nil")
|
||||
}
|
||||
|
||||
expiry, err := time.Parse(time.RFC3339, session.Values["oauth2_expiry"].(string))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
user := &AuthenticatedUser{
|
||||
ID: session.Values["id"].(string),
|
||||
Email: session.Values["email"].(string),
|
||||
OAuth2AccessToken: session.Values["oauth2_access_token"].(string),
|
||||
OAuth2RefreshToken: session.Values["oauth2_refresh_token"].(string),
|
||||
OAuth2TokenType: session.Values["oauth2_token_type"].(string),
|
||||
OAuth2Expiry: expiry,
|
||||
}
|
||||
|
||||
return user, nil
|
||||
}
|
||||
|
||||
func (h *BaseHandler) SetAuthenticatedUserForRequest(w http.ResponseWriter, r *http.Request, user *AuthenticatedUser) error {
|
||||
session, err := h.store.Get(r, sessionName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
session.Values["id"] = user.ID
|
||||
session.Values["email"] = user.Email
|
||||
session.Values["oauth2_access_token"] = user.OAuth2AccessToken
|
||||
session.Values["oauth2_refresh_token"] = user.OAuth2RefreshToken
|
||||
session.Values["oauth2_token_type"] = user.OAuth2TokenType
|
||||
session.Values["oauth2_expiry"] = user.OAuth2Expiry.Format(time.RFC3339)
|
||||
err = h.store.Save(r, w, session)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *BaseHandler) ClearAuthenticatedUserForRequest(w http.ResponseWriter, r *http.Request) error {
|
||||
session, err := h.store.Get(r, sessionName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
session.Options.MaxAge = -1
|
||||
err = h.store.Save(r, w, session)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type AuthenticatedHandler func(http.ResponseWriter, *http.Request, *AuthenticatedUser)
|
||||
|
||||
func (h *BaseHandler) Authenticated(next AuthenticatedHandler) func(http.ResponseWriter, *http.Request) {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
user, err := h.GetAuthenticatedUserForRequest(r)
|
||||
if err != nil {
|
||||
http.Redirect(w, r, "/oauth2/login", http.StatusTemporaryRedirect)
|
||||
return
|
||||
}
|
||||
if user.OAuth2Expiry.Before(time.Now()) {
|
||||
user, err = h.RefreshToken(w, r, user)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
}
|
||||
next(w, r, user)
|
||||
}
|
||||
}
|
|
@ -7,7 +7,7 @@ import (
|
|||
"code.tjo.space/mentos1386/zdravko/web/templates"
|
||||
)
|
||||
|
||||
func (h *BaseHandler) Settings(w http.ResponseWriter, r *http.Request) {
|
||||
func (h *BaseHandler) Settings(w http.ResponseWriter, r *http.Request, user *AuthenticatedUser) {
|
||||
ts, err := template.ParseFS(templates.Templates,
|
||||
"components/base.tmpl",
|
||||
"pages/settings.tmpl",
|
||||
|
@ -17,7 +17,7 @@ func (h *BaseHandler) Settings(w http.ResponseWriter, r *http.Request) {
|
|||
return
|
||||
}
|
||||
|
||||
err = ts.ExecuteTemplate(w, "base", nil)
|
||||
err = ts.ExecuteTemplate(w, "base", user)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
|
|
|
@ -2,4 +2,8 @@
|
|||
|
||||
{{define "main"}}
|
||||
<h1>The settings!</h1>
|
||||
<p>You are logged in as {{.Email}}.</p>
|
||||
<p>Your id is {{.ID}}.</p>
|
||||
<p>Your access expieres at {{.OAuth2Expiry}}.</p>
|
||||
<a href="/oauth2/logout">Logout</a>
|
||||
{{end}}
|
||||
|
|
Loading…
Reference in a new issue