Skip to content

Commit

Permalink
add auth integration
Browse files Browse the repository at this point in the history
  • Loading branch information
mpoegel committed Mar 4, 2024
1 parent 21f6611 commit 6a7d24c
Show file tree
Hide file tree
Showing 6 changed files with 199 additions and 8 deletions.
4 changes: 4 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ module github.com/mpoegel/rsvp.pizza
go 1.21

require (
github.com/coreos/go-oidc v2.2.1+incompatible
github.com/fauna/faunadb-go/v4 v4.2.0
github.com/gorilla/mux v1.8.0
github.com/mattn/go-sqlite3 v1.14.19
Expand All @@ -28,6 +29,7 @@ require (
github.com/kr/text v0.2.0 // indirect
github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/pquerna/cachecontrol v0.2.0 // indirect
github.com/prometheus/client_model v0.3.0 // indirect
github.com/prometheus/common v0.42.0 // indirect
github.com/prometheus/procfs v0.9.0 // indirect
Expand All @@ -36,12 +38,14 @@ require (
go.opencensus.io v0.24.0 // indirect
go.uber.org/atomic v1.7.0 // indirect
go.uber.org/multierr v1.6.0 // indirect
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 // indirect
golang.org/x/net v0.7.0 // indirect
golang.org/x/sys v0.6.0 // indirect
golang.org/x/text v0.7.0 // indirect
google.golang.org/appengine v1.6.7 // indirect
google.golang.org/genproto v0.0.0-20221227171554-f9683d7f8bef // indirect
google.golang.org/grpc v1.51.0 // indirect
google.golang.org/protobuf v1.30.0 // indirect
gopkg.in/square/go-jose.v2 v2.6.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
7 changes: 7 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
github.com/coreos/go-oidc v2.2.1+incompatible h1:mh48q/BqXqgjVHpy2ZY7WnWAbenxRjsz9N1i1YxjHAk=
github.com/coreos/go-oidc v2.2.1+incompatible/go.mod h1:CgnwVTmzoESiwO9qyAFEMiHoZ1nMCKZlZ9V6mm3/LKc=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
Expand Down Expand Up @@ -74,6 +76,8 @@ github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
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/pquerna/cachecontrol v0.2.0 h1:vBXSNuE5MYP9IJ5kjsdo8uq+w41jSPgvba2DEnkRx9k=
github.com/pquerna/cachecontrol v0.2.0/go.mod h1:NrUG3Z7Rdu85UNR3vm7SOsl1nFIeSiQnrHV5K9mBcUI=
github.com/prometheus/client_golang v1.15.1 h1:8tXpTmJbyH5lydzFPoxSIJ0J46jdh3tylbvM1xCv0LI=
github.com/prometheus/client_golang v1.15.1/go.mod h1:e9yaBhRPU2pPNsZwE+JdQl0KEt1N9XgF6zxWmaC0xOk=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
Expand Down Expand Up @@ -106,6 +110,7 @@ go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9i
go.uber.org/zap v1.23.0 h1:OjGQ5KQDEUawVHxNwQgPpiypGHOxo2mNZsOqTak4fFY=
go.uber.org/zap v1.23.0/go.mod h1:D+nX8jyLsMHMYrln8A0rJjFt/T/9/bGgIhAqxv5URuY=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 h1:psW17arqaxU48Z5kZ0CQnkZWQJsqcURM6tKiBApRjXI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
Expand Down Expand Up @@ -181,6 +186,8 @@ google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqw
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/square/go-jose.v2 v2.6.0 h1:NGk74WTnPKBNUhNzQX7PYcTLUjoq7mzKk2OKbvwk2iI=
gopkg.in/square/go-jose.v2 v2.6.0/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
Expand Down
18 changes: 16 additions & 2 deletions pkg/pizza/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ import (
"strings"
"time"

"go.uber.org/zap"
"gopkg.in/yaml.v2"
zap "go.uber.org/zap"
yaml "gopkg.in/yaml.v2"
)

var Log *zap.Logger
Expand All @@ -32,6 +32,7 @@ type Config struct {
FaunaSecret string `yaml:"faunaSecret"`
UseSQLite bool `yaml:"useSQLite"`
DBFile string `yaml:"dbFile"`
OAuth2 OAuth2Config
}

type CalendarConfig struct {
Expand All @@ -40,6 +41,13 @@ type CalendarConfig struct {
ID string `yaml:"id"`
}

type OAuth2Config struct {
ClientID string
ClientSecret string
RedirectURL string
RealmsURL string
}

func LoadConfig(filename string) (Config, error) {
config := Config{}
rawBytes, err := os.ReadFile(filename)
Expand Down Expand Up @@ -94,5 +102,11 @@ func LoadConfigEnv() Config {
FaunaSecret: loadStrEnv("FAUNADB_SECRET", ""),
UseSQLite: loadBoolEnv("USE_SQLITE", true),
DBFile: loadStrEnv("DBFILE", "pizza.db"),
OAuth2: OAuth2Config{
ClientID: loadStrEnv("OAUTH2_CLIENT_ID", ""),
ClientSecret: loadStrEnv("OAUTH2_CLIENT_SECRET", ""),
RedirectURL: loadStrEnv("OAUTH2_REDIRECT", "http://localhost/login/callback"),
RealmsURL: loadStrEnv("REALMS_URL", "http://localhost:8080/auth/realms/pizza"),
},
}
}
173 changes: 168 additions & 5 deletions pkg/pizza/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,11 @@ import (
"text/template"
"time"

"github.com/gorilla/mux"
"go.uber.org/zap"
oidc "github.com/coreos/go-oidc"
uuid "github.com/google/uuid"
mux "github.com/gorilla/mux"
zap "go.uber.org/zap"
oauth2 "golang.org/x/oauth2"
)

var EventDuration = time.Hour * 4
Expand All @@ -28,13 +31,20 @@ type Server struct {
calendar *Calendar
config Config

oauth2Provider *oidc.Provider
oauth2Conf oauth2.Config
verifier *oidc.IDTokenVerifier

indexGetMetric CounterMetric
submitPostMetric CounterMetric
wrappedGetMetric CounterMetric
requestErrorMetric CounterMetric
internalErrorMetric CounterMetric

wrapped map[int]WrappedData
wrapped map[int]WrappedData
sessions map[string]*TokenClaims

// activeState string
}

func NewServer(config Config, metricsReg MetricsRegistry) (Server, error) {
Expand All @@ -60,6 +70,14 @@ func NewServer(config Config, metricsReg MetricsRegistry) (Server, error) {
return Server{}, err
}

// ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
// defer cancel()
ctx := context.Background()
provider, err := oidc.NewProvider(ctx, config.OAuth2.RealmsURL)
if err != nil {
return Server{}, err
}

s := Server{
s: http.Server{
Addr: fmt.Sprintf("0.0.0.0:%d", config.Port),
Expand All @@ -70,6 +88,19 @@ func NewServer(config Config, metricsReg MetricsRegistry) (Server, error) {
store: NewStore(accessor),
calendar: NewCalendar(googleCal),
config: config,

oauth2Provider: provider,
oauth2Conf: oauth2.Config{
ClientID: config.OAuth2.ClientID,
ClientSecret: config.OAuth2.ClientSecret,
RedirectURL: config.OAuth2.RedirectURL,
Endpoint: provider.Endpoint(),
Scopes: []string{oidc.ScopeOpenID, "profile", "email"},
},
verifier: provider.Verifier(&oidc.Config{
ClientID: config.OAuth2.ClientID,
}),

indexGetMetric: metricsReg.NewCounterMetric("pizza_requests",
map[string]string{"method": "get", "path": "/"}),
submitPostMetric: metricsReg.NewCounterMetric("pizza_requests",
Expand All @@ -81,13 +112,16 @@ func NewServer(config Config, metricsReg MetricsRegistry) (Server, error) {
internalErrorMetric: metricsReg.NewCounterMetric("pizza_errors",
map[string]string{"statusCode": "500"}),

wrapped: map[int]WrappedData{},
wrapped: map[int]WrappedData{},
sessions: map[string]*TokenClaims{},
}

r.HandleFunc("/", s.HandleIndex)
r.HandleFunc("/submit", s.HandleSubmit)
r.HandleFunc("/wrapped", s.HandledWrapped)
r.PathPrefix("/static/").Handler(http.StripPrefix("/static/", http.FileServer(http.Dir(config.StaticDir))))
r.HandleFunc("/login", s.HandleLogin)
r.HandleFunc("/login/callback", s.HandleLoginCallback)

return s, nil
}
Expand Down Expand Up @@ -169,17 +203,41 @@ type IndexFridayData struct {

type PageData struct {
FridayTimes []IndexFridayData
Name string
}

func (s *Server) HandleIndex(w http.ResponseWriter, r *http.Request) {
s.indexGetMetric.Increment()

var claims *TokenClaims
for _, cookie := range r.Cookies() {
if cookie.Name == "session" {
var ok bool
claims, ok = s.sessions[cookie.Value]
// either bad session or auth has expired
if !ok || time.Now().After(time.Unix(claims.Exp, 0)) {
delete(s.sessions, cookie.Value)
http.Redirect(w, r, "/login", http.StatusFound)
return
}
}
}
if claims == nil {
http.Redirect(w, r, "/login", http.StatusFound)
return
}

Log.Info("welcome", zap.String("name", claims.Name))

plate, err := template.ParseFiles(path.Join(s.config.StaticDir, "html/index.html"))
if err != nil {
Log.Error("template index failure", zap.Error(err))
s.Handle500(w, r)
return
}
data := PageData{}
data := PageData{
Name: claims.GivenName,
}

fridays, err := s.store.GetUpcomingFridays(30)
if err != nil {
Expand Down Expand Up @@ -307,6 +365,111 @@ func (s *Server) HandleSubmit(w http.ResponseWriter, r *http.Request) {
}
}

func (s *Server) HandleLogin(w http.ResponseWriter, r *http.Request) {
state := uuid.New()
rawAccessToken := r.Header.Get("Authorization")
if rawAccessToken == "" {
s.sessions[state.String()] = nil
http.Redirect(w, r, s.oauth2Conf.AuthCodeURL(state.String()), http.StatusFound)
return
}

authParts := strings.Split(rawAccessToken, " ")
if len(authParts) != 2 {
w.WriteHeader(400)
return
}

ctx := context.Background()
_, err := s.verifier.Verify(ctx, authParts[1])
if err != nil {
s.sessions[state.String()] = nil
http.Redirect(w, r, s.oauth2Conf.AuthCodeURL(state.String()), http.StatusFound)
return
}

w.WriteHeader(http.StatusOK)
w.Write([]byte("ok"))
}

type TokenClaims struct {
Exp int64 `json:"exp"`
Iat int64 `json:"iat"`
AuthTime int64 `json:"auth_time"`
Jti string `json:"jti"`
Iss string `json:"iss"`
Aud string `json:"aud"`
Sub string `json:"sub"`
Typ string `json:"typ"`
Azp string `json:"azp"`
SessionState string `json:"session_state"`
At_hash string `json:"at_hash"`
Acr string `json:"acr"`
Sid string `json:"sid"`
EmailVerified bool `json:"email_verified"`
Name string `json:"name"`
PreferredUsername string `json:"preferred_username"`
GivenName string `json:"given_name"`
FamilyName string `json:"family_name"`
Email string `json:"email"`
}

func (s *Server) HandleLoginCallback(w http.ResponseWriter, r *http.Request) {
state := r.URL.Query().Get("state")
if _, ok := s.sessions[state]; !ok {
Log.Warn("state did not match")
http.Error(w, "state did not match", http.StatusBadRequest)
return
}

ctx := context.Background()
oauth2Token, err := s.oauth2Conf.Exchange(ctx, r.URL.Query().Get("code"))
if err != nil {
Log.Warn("failed to exchange code for token", zap.Error(err))
http.Error(w, "auth error", http.StatusInternalServerError)
return
}

rawIDToken, ok := oauth2Token.Extra("id_token").(string)
if !ok {
Log.Warn("no id_token field in oauth2 token")
http.Error(w, "auth error", http.StatusInternalServerError)
return
}

idToken, err := s.verifier.Verify(ctx, rawIDToken)
if err != nil {
Log.Warn("failed to verify ID token", zap.Error(err))
http.Error(w, "auth error", http.StatusInternalServerError)
return
}

var claims TokenClaims
if err := idToken.Claims(&claims); err != nil {
Log.Warn("failed to get claims", zap.Error(err))
http.Error(w, "auth error", http.StatusInternalServerError)
return
}

Log.Info("login success", zap.Any("claims", claims))
cookie := &http.Cookie{
Name: "session",
Value: state,
Path: "/",
Expires: time.Now().AddDate(0, 0, 10),
HttpOnly: true,
SameSite: http.SameSiteNoneMode,
}
if err := cookie.Valid(); err != nil {
Log.Warn("bad cookie", zap.Error(err))
}
http.SetCookie(w, cookie)
r.AddCookie(cookie)

s.sessions[state] = &claims
http.Redirect(w, r, "/", http.StatusSeeOther)
}

type WrappedPageData struct {
Email string
Name string
Expand Down
2 changes: 1 addition & 1 deletion static/css/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -65,4 +65,4 @@ body {
#submit {
margin-left: 0px;
}
}
}
3 changes: 3 additions & 0 deletions static/html/index.html
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
<!DOCTYPE html>
<html>

<head>
Expand All @@ -8,6 +9,8 @@
<body>
<h2>RSVP For Pizza</h2>

<p>Hi {{.Name}}</p>

<form method="get" action="/submit">
{{range .FridayTimes}}
<input type="checkbox" id="{{.Date}}" name="date" value="{{.ID}}">
Expand Down

0 comments on commit 6a7d24c

Please sign in to comment.