DEVELOPMENT ENVIRONMENT

~liljamo/tixe

eebb5024003fe8e1fe1aec83ef22011b682d2d6c — Jonni Liljamo 1 year, 5 months ago 33b6fe9
feat: login and logout with oidc
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")
}