From eebb5024003fe8e1fe1aec83ef22011b682d2d6c Mon Sep 17 00:00:00 2001 From: Jonni Liljamo Date: Mon, 19 Jun 2023 23:25:01 +0300 Subject: [PATCH] feat: login and logout with oidc --- auth/auth.go | 39 +++++++ auth/oidc.go | 28 +++++ config/config.go | 24 ++++- docker-compose.yaml | 7 +- go.mod | 17 ++- go.sum | 40 +++++++ handlers/auth.go | 46 ++++++++ handlers/authlogin.go | 40 +++++++ handlers/authlogout.go | 31 ++++++ handlers/login.go | 13 +++ middlewares/auth.go | 25 +++++ static/styles.css | 156 ++++++++++++++++++++++++++++ tailwind.config.js | 10 +- template/templates/common/base.tmpl | 27 ++++- template/templates/login.tmpl | 9 ++ tixe.go | 49 +++++++-- 16 files changed, 539 insertions(+), 22 deletions(-) create mode 100644 auth/auth.go create mode 100644 auth/oidc.go create mode 100644 handlers/auth.go create mode 100644 handlers/authlogin.go create mode 100644 handlers/authlogout.go create mode 100644 handlers/login.go create mode 100644 middlewares/auth.go create mode 100644 template/templates/login.tmpl diff --git a/auth/auth.go b/auth/auth.go new file mode 100644 index 0000000..7600ce0 --- /dev/null +++ b/auth/auth.go @@ -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) +} diff --git a/auth/oidc.go b/auth/oidc.go new file mode 100644 index 0000000..9a6100f --- /dev/null +++ b/auth/oidc.go @@ -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 +} diff --git a/config/config.go b/config/config.go index 0e0890c..f999cdd 100644 --- a/config/config.go +++ b/config/config.go @@ -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", ""), } } diff --git a/docker-compose.yaml b/docker-compose.yaml index cd97064..b1262ea 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -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 diff --git a/go.mod b/go.mod index 0e00eaf..6d681de 100644 --- a/go.mod +++ b/go.mod @@ -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 ) diff --git a/go.sum b/go.sum index 72ca3fb..ebdd88e 100644 --- a/go.sum +++ b/go.sum @@ -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= diff --git a/handlers/auth.go b/handlers/auth.go new file mode 100644 index 0000000..d76e37a --- /dev/null +++ b/handlers/auth.go @@ -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, "/") + } +} diff --git a/handlers/authlogin.go b/handlers/authlogin.go new file mode 100644 index 0000000..2b36e2e --- /dev/null +++ b/handlers/authlogin.go @@ -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 +} diff --git a/handlers/authlogout.go b/handlers/authlogout.go new file mode 100644 index 0000000..1003987 --- /dev/null +++ b/handlers/authlogout.go @@ -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, "/") +} diff --git a/handlers/login.go b/handlers/login.go new file mode 100644 index 0000000..a9f4191 --- /dev/null +++ b/handlers/login.go @@ -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) +} diff --git a/middlewares/auth.go b/middlewares/auth.go new file mode 100644 index 0000000..a8a11c9 --- /dev/null +++ b/middlewares/auth.go @@ -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() + } +} diff --git a/static/styles.css b/static/styles.css index 5761569..298f7f2 100644 --- a/static/styles.css +++ b/static/styles.css @@ -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; +} diff --git a/tailwind.config.js b/tailwind.config.js index 1386fbf..da247e2 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -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: [], } diff --git a/template/templates/common/base.tmpl b/template/templates/common/base.tmpl index c249c27..5098c63 100644 --- a/template/templates/common/base.tmpl +++ b/template/templates/common/base.tmpl @@ -10,8 +10,31 @@
-
- {{ template "content" . }} +
+
+ +
+
+
+ {{ if eq .notauthed true }} + Not logged in + {{ else if ne .profile nil }} + Logged in as {{ .profile.name }} + Logout + {{ end }} +
+
+
+ {{ template "content" . }} +
diff --git a/template/templates/login.tmpl b/template/templates/login.tmpl new file mode 100644 index 0000000..863b939 --- /dev/null +++ b/template/templates/login.tmpl @@ -0,0 +1,9 @@ +{{ define "content" }} +

Login

+ + Liljamo Auth + +{{ end }} diff --git a/tixe.go b/tixe.go index 9e5bd98..34d76d5 100644 --- a/tixe.go +++ b/tixe.go @@ -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") } -- 2.44.1