A auth/auth.go => auth/auth.go +39 -0
@@ 0,0 1,39 @@
+package auth
+
+import (
+ "context"
+ "errors"
+
+ "github.com/coreos/go-oidc/v3/oidc"
+ "golang.org/x/oauth2"
+)
+
+type Auth struct {
+ *oidc.Provider
+ oauth2.Config
+}
+
+func NewAuth() (*Auth, error) {
+ provider, config, err := NewProviderAndConfig()
+ if err != nil {
+ return nil, err
+ }
+
+ return &Auth{
+ Provider: provider,
+ Config: config,
+ }, nil
+}
+
+func (a *Auth) VerifyIDToken(c context.Context, token *oauth2.Token) (*oidc.IDToken, error) {
+ idToken, ok := token.Extra("id_token").(string)
+ if !ok {
+ return nil, errors.New("No id_token field in oauth2 token")
+ }
+
+ oidcConfig := &oidc.Config{
+ ClientID: a.ClientID,
+ }
+
+ return a.Verifier(oidcConfig).Verify(c, idToken)
+}
A auth/oidc.go => auth/oidc.go +28 -0
@@ 0,0 1,28 @@
+package auth
+
+import (
+ "context"
+ "log"
+ "tixe/config"
+
+ "github.com/coreos/go-oidc/v3/oidc"
+ "golang.org/x/oauth2"
+)
+
+func NewProviderAndConfig() (*oidc.Provider, oauth2.Config, error) {
+ provider, err := oidc.NewProvider(context.Background(), "https://" + config.TixeConfig.OidcDomain)
+ if err != nil {
+ log.Printf("[tixe/auth] Failed to create new custom provider")
+ return nil, oauth2.Config{}, err
+ }
+
+ config := oauth2.Config{
+ ClientID: config.TixeConfig.OidcClientID,
+ ClientSecret: config.TixeConfig.OidcSecret,
+ RedirectURL: config.TixeConfig.Scheme + "://" + config.TixeConfig.Host + "/auth",
+ Endpoint: provider.Endpoint(),
+ Scopes: []string{oidc.ScopeOpenID, "profile", "email"},
+ }
+
+ return provider, config, nil
+}
M config/config.go => config/config.go +20 -4
@@ 8,27 8,43 @@ import (
var TixeConfig *Config
type Config struct {
+ // Tixe host, e.g. tixe.liljamo.dev
+ // Defaults to 127.0.0.1:8080 for local testing
+ Host string
+ // Tixe scheme, http or https (default)
+ Scheme string
+
PsqlHost string
+ // Postgres port, defaults to 5432
PsqlPort string
PsqlUser string
PsqlPwd string
PsqlDb string
- OidcGithub bool
- OidcCustom bool
+ CookieSecret string
+
+ OidcDomain string
+ OidcClientID string
+ OidcSecret string
}
func ParseConfig() {
log.Print("[tixe/config] Parsing config")
TixeConfig = &Config{
+ Host: util.LoadVar("TIXE_HOST", "127.0.0.1:8080"),
+ Scheme: util.LoadVar("TIXE_SCHEME", "http"),
+
PsqlHost: util.LoadVar("TIXE_PSQL_HOST", ""),
PsqlPort: util.LoadVar("TIXE_PSQL_PORT", "5432"),
PsqlUser: util.LoadVar("TIXE_PSQL_USER", ""),
PsqlPwd: util.LoadVar("TIXE_PSQL_PASSWORD", ""),
PsqlDb: util.LoadVar("TIXE_PSQL_DB", ""),
- OidcGithub: util.LoadVarBool("TIXE_OIDC_GITHUB", false),
- OidcCustom: util.LoadVarBool("TIXE_OIDC_CUSTOM", false),
+ CookieSecret: util.LoadVar("TIXE_COOKIE_SECRET", ""),
+
+ OidcDomain: util.LoadVar("TIXE_OIDC_DOMAIN", ""),
+ OidcClientID: util.LoadVar("TIXE_OIDC_CLIENTID", ""),
+ OidcSecret: util.LoadVar("TIXE_OIDC_SECRET", ""),
}
}
M docker-compose.yaml => docker-compose.yaml +4 -3
@@ 35,11 35,12 @@ services:
TZ: Europe/Helsinki
GIN_MODE: release # or "debug" for debug logs
TIXE_PSQL_HOST: tixedb
- TIXE_PSQL_PORT: 5432
TIXE_PSQL_USER: tixe
TIXE_PSQL_PASSWORD: tixe
TIXE_PSQL_DB: tixe
- TIXE_OIDC_GITHUB: false
- TIXE_OIDC_CUSTOM: true
+ TIXE_COOKIE_SECRET: secret
+ TIXE_OIDC_DOMAIN: auth.example.com
+ TIXE_OIDC_CLIENTID: tixeclient
+ TIXE_OIDC_SECRET: secret
depends_on:
- tixedb
M go.mod => go.mod +13 -4
@@ 7,12 7,19 @@ require github.com/gin-gonic/gin v1.9.1
require (
github.com/bytedance/sonic v1.9.1 // indirect
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect
+ github.com/coreos/go-oidc/v3 v3.6.0 // indirect
github.com/gabriel-vasile/mimetype v1.4.2 // indirect
+ github.com/gin-contrib/sessions v0.0.5 // indirect
github.com/gin-contrib/sse v0.1.0 // indirect
+ github.com/go-jose/go-jose/v3 v3.0.0 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-playground/validator/v10 v10.14.1 // indirect
github.com/goccy/go-json v0.10.2 // indirect
+ github.com/golang/protobuf v1.5.2 // indirect
+ github.com/gorilla/context v1.1.1 // indirect
+ github.com/gorilla/securecookie v1.1.1 // indirect
+ github.com/gorilla/sessions v1.2.1 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect
github.com/jackc/pgx/v5 v5.4.0 // indirect
@@ 27,11 34,13 @@ require (
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.2.11 // indirect
golang.org/x/arch v0.3.0 // indirect
- golang.org/x/crypto v0.9.0 // indirect
- golang.org/x/net v0.10.0 // indirect
+ golang.org/x/crypto v0.10.0 // indirect
+ golang.org/x/net v0.11.0 // indirect
+ golang.org/x/oauth2 v0.9.0 // indirect
golang.org/x/sync v0.1.0 // indirect
- golang.org/x/sys v0.8.0 // indirect
- golang.org/x/text v0.9.0 // indirect
+ golang.org/x/sys v0.9.0 // indirect
+ golang.org/x/text v0.10.0 // indirect
+ google.golang.org/appengine v1.6.7 // indirect
google.golang.org/protobuf v1.30.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
M go.sum => go.sum +40 -0
@@ 4,14 4,20 @@ github.com/bytedance/sonic v1.9.1/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZX
github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY=
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 h1:qSGYFH7+jGhDF8vLC+iwCD4WpbV1EBDSzWkJODFLams=
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk=
+github.com/coreos/go-oidc/v3 v3.6.0 h1:AKVxfYw1Gmkn/w96z0DbT/B/xFnzTd3MkZvWLjF4n/o=
+github.com/coreos/go-oidc/v3 v3.6.0/go.mod h1:ZpHUsHBucTUj6WOkrP4E20UPynbLZzhTQ1XKCXkxyPc=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU=
github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA=
+github.com/gin-contrib/sessions v0.0.5 h1:CATtfHmLMQrMNpJRgzjWXD7worTh7g7ritsQfmF+0jE=
+github.com/gin-contrib/sessions v0.0.5/go.mod h1:vYAuaUPqie3WUSsft6HUlCjlwwoJQs97miaG2+7neKY=
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg=
github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU=
+github.com/go-jose/go-jose/v3 v3.0.0 h1:s6rrhirfEP/CGIoc6p+PZAeogN2SxKav6Wp7+dyMWVo=
+github.com/go-jose/go-jose/v3 v3.0.0/go.mod h1:RNkWWRld676jZEYoV3+XK8L2ZnNSvIsxFMht0mSX+u8=
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
@@ 20,9 26,19 @@ github.com/go-playground/validator/v10 v10.14.1 h1:9c50NUPC30zyuKprjL3vNZ0m5oG+j
github.com/go-playground/validator/v10 v10.14.1/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU=
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
+github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
+github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw=
+github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
+github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
+github.com/gorilla/context v1.1.1 h1:AWwleXJkX/nhcU9bZSnZoi3h/qGYqQAGhq6zZe/aQW8=
+github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=
+github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ=
+github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4=
+github.com/gorilla/sessions v1.2.1 h1:DHd3rPN5lE3Ts3D8rKkQ8x/0kqfeNmBAaiSi+o7FsgI=
+github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM=
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk=
@@ 52,6 68,7 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
+github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
@@ 65,23 82,46 @@ github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZ
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
golang.org/x/arch v0.3.0 h1:02VY4/ZcO/gBOH6PUaoiptASxtXU10jazRCP865E97k=
golang.org/x/arch v0.3.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
+golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
+golang.org/x/crypto v0.0.0-20190911031432-227b76d455e7/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.9.0 h1:LF6fAI+IutBocDJ2OT0Q1g8plpYljMZ4+lty+dsqw3g=
golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0=
+golang.org/x/crypto v0.10.0 h1:LKqV2xt9+kDzSTfOhx4FrkEBcMrAgHSYgzywV9zcGmM=
+golang.org/x/crypto v0.10.0/go.mod h1:o4eNf7Ede1fv+hwOwZsTHl9EsPFO6q6ZvYR8vYfY45I=
+golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
+golang.org/x/net v0.11.0 h1:Gi2tvZIJyBtO9SDr1q9h5hEQCp/4L2RQ+ar0qjx2oNU=
+golang.org/x/net v0.11.0/go.mod h1:2L/ixqYpgIVXmeoSA/4Lu7BzTG4KIyPIryS4IsOd1oQ=
+golang.org/x/oauth2 v0.9.0 h1:BPpt2kU7oMRq3kCHAA1tbSEshXRw1LpG2ztgDwrzuAs=
+golang.org/x/oauth2 v0.9.0/go.mod h1:qYgFZaFiu6Wg24azG8bdV52QJXJGbZzIIsRCdVKzbLw=
golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.9.0 h1:KS/R3tvhPqvJvwcKfnBHJwwthS11LRhmM5D59eEXa0s=
+golang.org/x/sys v0.9.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
+golang.org/x/text v0.10.0 h1:UpjohKhiEgNc0CSauXmwYftY1+LlaC75SJwh0SgCX58=
+golang.org/x/text v0.10.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
+golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c=
+google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
+google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng=
google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
A handlers/auth.go => handlers/auth.go +46 -0
@@ 0,0 1,46 @@
+package handlers
+
+import (
+ "net/http"
+ "tixe/auth"
+
+ "github.com/gin-contrib/sessions"
+ "github.com/gin-gonic/gin"
+)
+
+func AuthCallback(auth *auth.Auth) gin.HandlerFunc {
+ return func(c *gin.Context) {
+ session := sessions.Default(c)
+ if c.Query("state") != session.Get("state") {
+ c.String(http.StatusBadRequest, "Invalid state parameter!")
+ return
+ }
+
+ token, err := auth.Exchange(c.Request.Context(), c.Query("code"))
+ if err != nil {
+ c.String(http.StatusUnauthorized, "Failed to exchange authorization code for token!")
+ return
+ }
+
+ idToken, err := auth.VerifyIDToken(c.Request.Context(), token)
+ if err != nil {
+ c.String(http.StatusInternalServerError, "Failed to verify ID token!")
+ return
+ }
+
+ var profile map[string]interface{}
+ if err := idToken.Claims(&profile); err != nil {
+ c.String(http.StatusInternalServerError, err.Error())
+ return
+ }
+
+ session.Set("access_token", token.AccessToken)
+ session.Set("profile", profile)
+ if err := session.Save(); err != nil {
+ c.String(http.StatusInternalServerError, err.Error())
+ return
+ }
+
+ c.Redirect(http.StatusTemporaryRedirect, "/")
+ }
+}
A handlers/authlogin.go => handlers/authlogin.go +40 -0
@@ 0,0 1,40 @@
+package handlers
+
+import (
+ "crypto/rand"
+ "encoding/base64"
+ "net/http"
+ "tixe/auth"
+
+ "github.com/gin-contrib/sessions"
+ "github.com/gin-gonic/gin"
+)
+
+func AuthLogin(auth *auth.Auth) gin.HandlerFunc {
+ return func(c *gin.Context) {
+ state, err := randomState()
+ if err != nil {
+ c.String(http.StatusInternalServerError, err.Error())
+ return
+ }
+
+ session := sessions.Default(c)
+ session.Set("state", state)
+ if err := session.Save(); err != nil {
+ c.String(http.StatusInternalServerError, err.Error())
+ return
+ }
+
+ c.Redirect(http.StatusTemporaryRedirect, auth.AuthCodeURL(state))
+ }
+}
+
+func randomState() (string, error) {
+ buf := make([]byte, 32)
+ _, err := rand.Read(buf)
+ if err != nil {
+ return "", nil
+ }
+
+ return base64.StdEncoding.EncodeToString(buf), nil
+}
A handlers/authlogout.go => handlers/authlogout.go +31 -0
@@ 0,0 1,31 @@
+package handlers
+
+import (
+ "net/http"
+
+ "github.com/gin-contrib/sessions"
+ "github.com/gin-gonic/gin"
+)
+
+func AuthLogout(c *gin.Context) {
+ session := sessions.Default(c)
+ session.Clear()
+ _ = session.Save()
+
+ /*
+ logoutUrl, err := url.Parse("https://" + config.TixeConfig.OidcDomain + "/logout")
+ if err != nil {
+ c.String(http.StatusInternalServerError, err.Error())
+ return
+ }
+
+ params := url.Values{}
+ params.Add("returnTo", config.TixeConfig.Scheme + "://" + config.TixeConfig.Host)
+ params.Add("client_id", config.TixeConfig.OidcClientID)
+ logoutUrl.RawQuery = params.Encode()
+
+ c.Redirect(http.StatusTemporaryRedirect, logoutUrl.String())
+ */
+
+ c.Redirect(http.StatusTemporaryRedirect, "/")
+}
A handlers/login.go => handlers/login.go +13 -0
@@ 0,0 1,13 @@
+package handlers
+
+import (
+ "net/http"
+ "tixe/template"
+
+ "github.com/gin-gonic/gin"
+)
+
+func Login(c *gin.Context) {
+ html := template.TmplEngine.Render("login.tmpl", map[string]interface{}{"notauthed": true})
+ c.Data(http.StatusOK, "text/html", html)
+}
A middlewares/auth.go => middlewares/auth.go +25 -0
@@ 0,0 1,25 @@
+package middlewares
+
+import (
+ "net/http"
+
+ "github.com/gin-contrib/sessions"
+ "github.com/gin-gonic/gin"
+)
+
+func IsAuthenticated(c *gin.Context) {
+ if sessions.Default(c).Get("profile") == nil {
+ c.Redirect(http.StatusSeeOther, "/login")
+ } else {
+ c.Next()
+ }
+}
+
+func CanLogin(c *gin.Context) {
+ if sessions.Default(c).Get("profile") != nil {
+ // Don't allow the login page if logged in
+ c.Redirect(http.StatusSeeOther, "/")
+ } else {
+ c.Next()
+ }
+}
M static/styles.css => static/styles.css +156 -0
@@ 522,11 522,167 @@ video {
--tw-backdrop-sepia: ;
}
+.flex {
+ display: flex;
+}
+
+.w-full {
+ width: 100%;
+}
+
+.w-max {
+ width: -moz-max-content;
+ width: max-content;
+}
+
+.min-w-max {
+ min-width: -moz-max-content;
+ min-width: max-content;
+}
+
+.max-w-7xl {
+ max-width: 80rem;
+}
+
+.max-w-4xl {
+ max-width: 56rem;
+}
+
+.flex-col {
+ flex-direction: column;
+}
+
+.items-center {
+ align-items: center;
+}
+
+.gap-2 {
+ gap: 0.5rem;
+}
+
+.gap-4 {
+ gap: 1rem;
+}
+
+.self-end {
+ align-self: flex-end;
+}
+
+.rounded-md {
+ border-radius: 0.375rem;
+}
+
+.border-2 {
+ border-width: 2px;
+}
+
+.border-b-2 {
+ border-bottom-width: 2px;
+}
+
+.bg-slate-50 {
+ --tw-bg-opacity: 1;
+ background-color: rgb(248 250 252 / var(--tw-bg-opacity));
+}
+
+.bg-gradient-to-br {
+ background-image: linear-gradient(to bottom right, var(--tw-gradient-stops));
+}
+
+.from-slate-600 {
+ --tw-gradient-from: #475569 var(--tw-gradient-from-position);
+ --tw-gradient-to: rgb(71 85 105 / 0) var(--tw-gradient-to-position);
+ --tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to);
+}
+
+.via-blue-400 {
+ --tw-gradient-to: rgb(96 165 250 / 0) var(--tw-gradient-to-position);
+ --tw-gradient-stops: var(--tw-gradient-from), #60a5fa var(--tw-gradient-via-position), var(--tw-gradient-to);
+}
+
+.to-rose-500 {
+ --tw-gradient-to: #f43f5e var(--tw-gradient-to-position);
+}
+
+.bg-size-200 {
+ background-size: 200% 200%;
+}
+
+.bg-clip-text {
+ -webkit-background-clip: text;
+ background-clip: text;
+}
+
+.bg-pos-0 {
+ background-position: 0% 0%;
+}
+
+.p-2 {
+ padding: 0.5rem;
+}
+
.p-4 {
padding: 1rem;
}
+.text-lg {
+ font-size: 1.125rem;
+ line-height: 1.75rem;
+}
+
+.text-sm {
+ font-size: 0.875rem;
+ line-height: 1.25rem;
+}
+
+.text-xl {
+ font-size: 1.25rem;
+ line-height: 1.75rem;
+}
+
+.font-bold {
+ font-weight: 700;
+}
+
.text-blue-500 {
--tw-text-opacity: 1;
color: rgb(59 130 246 / var(--tw-text-opacity));
}
+
+.text-transparent {
+ color: transparent;
+}
+
+.drop-shadow-md {
+ --tw-drop-shadow: drop-shadow(0 4px 3px rgb(0 0 0 / 0.07)) drop-shadow(0 2px 2px rgb(0 0 0 / 0.06));
+ filter: var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow);
+}
+
+.transition-all {
+ transition-property: all;
+ transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
+ transition-duration: 150ms;
+}
+
+.transition-colors {
+ transition-property: color, background-color, border-color, text-decoration-color, fill, stroke;
+ transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
+ transition-duration: 150ms;
+}
+
+.duration-500 {
+ transition-duration: 500ms;
+}
+
+.hover\:bg-slate-200:hover {
+ --tw-bg-opacity: 1;
+ background-color: rgb(226 232 240 / var(--tw-bg-opacity));
+}
+
+.hover\:bg-pos-100:hover {
+ background-position: 100% 100%;
+}
+
+.hover\:underline:hover {
+ text-decoration-line: underline;
+}
M tailwind.config.js => tailwind.config.js +9 -1
@@ 2,7 2,15 @@
module.exports = {
content: ["./template/templates/**/*.tmpl"],
theme: {
- extend: {},
+ extend: {
+ backgroundSize: {
+ "size-200": "200% 200%",
+ },
+ backgroundPosition: {
+ "pos-0": "0% 0%",
+ "pos-100": "100% 100%",
+ },
+ },
},
plugins: [],
}
M template/templates/common/base.tmpl => template/templates/common/base.tmpl +25 -2
@@ 10,8 10,31 @@
</head>
<body>
<main>
- <div class="p-4">
- {{ template "content" . }}
+ <div class="flex flex-col w-full items-center p-4">
+ <div class="flex w-full max-w-4xl items-center gap-4">
+ <div class="w-max">
+ <a class="bg-size-200 bg-pos-0 hover:bg-pos-100
+ bg-gradient-to-br from-slate-600 via-blue-400 to-rose-500
+ bg-clip-text text-lg font-bold text-transparent
+ transition-all duration-500"
+ href="/">
+ Tixë
+ </a>
+ </div>
+ <div class="flex w-full">
+ </div>
+ <div class="flex flex-col min-w-max">
+ {{ if eq .notauthed true }}
+ Not logged in
+ {{ else if ne .profile nil }}
+ Logged in as {{ .profile.name }}
+ <a class="self-end text-sm hover:underline" href="/auth/logout">Logout</a>
+ {{ end }}
+ </div>
+ </div>
+ <div class="flex flex-col w-full max-w-4xl items-center gap-2">
+ {{ template "content" . }}
+ </div>
</div>
</main>
</body>
A template/templates/login.tmpl => template/templates/login.tmpl +9 -0
@@ 0,0 1,9 @@
+{{ define "content" }}
+<p class="text-xl p-2 border-b-2">Login</p>
+<a class="w-max p-2 border-2
+ rounded-md drop-shadow-md
+ bg-slate-50 hover:bg-slate-200 transition-colors"
+ href="/auth/login">
+ Liljamo Auth
+</a>
+{{ end }}
M tixe.go => tixe.go +41 -8
@@ 1,13 1,19 @@
package main
import (
+ "encoding/gob"
"log"
"net/http"
"tixe/api"
+ "tixe/auth"
"tixe/config"
"tixe/db"
+ "tixe/handlers"
+ "tixe/middlewares"
"tixe/template"
+ "github.com/gin-contrib/sessions"
+ "github.com/gin-contrib/sessions/cookie"
"github.com/gin-gonic/gin"
)
@@ 16,27 22,49 @@ func ping(c *gin.Context) {
}
func root(c *gin.Context) {
- html := template.TmplEngine.Render("index.tmpl", map[string]interface{}{"title": "tixë"})
+ session := sessions.Default(c)
+ profile := session.Get("profile")
+
+ html := template.TmplEngine.Render("index.tmpl", map[string]interface{}{"title": "tixë", "profile": profile})
c.Data(http.StatusOK, "text/html", html)
}
func handleNoRoute(c *gin.Context) {
- html := template.TmplEngine.Render("404.tmpl", map[string]interface{}{"title": "tixë"})
+ session := sessions.Default(c)
+ profile := session.Get("profile")
+
+ html := template.TmplEngine.Render("404.tmpl", map[string]interface{}{"profile": profile})
c.Data(http.StatusNotFound, "text/html", html)
}
-func setupRouter() *gin.Engine {
+func setupRouter(auth *auth.Auth) *gin.Engine {
r := gin.Default()
r.Static("/static", "./static")
+ gob.Register(map[string]interface{}{})
+ store := cookie.NewStore([]byte(config.TixeConfig.CookieSecret))
+ r.Use(sessions.Sessions("auth-session", store))
+
r.NoRoute(handleNoRoute)
+ r.GET("/login", middlewares.CanLogin, handlers.Login)
- r.GET("/", root)
+ authRoute := r.Group("/auth")
+ {
+ authRoute.GET("/login", handlers.AuthLogin(auth))
+ authRoute.GET("/", handlers.AuthCallback(auth))
+ authRoute.GET("/logout", handlers.AuthLogout)
+ }
- apiRoute := r.Group("/api")
+ reqAuth := r.Group("/")
+ reqAuth.Use(middlewares.IsAuthenticated)
{
- apiRoute.GET("/", api.Root)
- apiRoute.GET("/ping", ping)
+ reqAuth.GET("/", root)
+
+ apiRoute := reqAuth.Group("/api")
+ {
+ apiRoute.GET("/", api.Root)
+ apiRoute.GET("/ping", ping)
+ }
}
return r
@@ 56,7 84,12 @@ func main() {
log.Fatalf("[tixe] Creating a new TemplateEngine failed, '%s'", err)
}
- r := setupRouter()
+ auth, err := auth.NewAuth()
+ if err != nil {
+ log.Fatalf("[tixe] Creating a new authenticator failed, '%s'", err)
+ }
+
+ r := setupRouter(auth)
r.Run(":8080")
}