Implement login flow
This commit is contained in:
parent
027ed72fdc
commit
c0e9e88dba
7 changed files with 624 additions and 95 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -1 +1,3 @@
|
|||
/.idea/
|
||||
resourceapp.json
|
||||
authapp.json
|
||||
|
|
|
@ -3,76 +3,182 @@ package main
|
|||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"html/template"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
client2 "github.com/go-openapi/runtime/client"
|
||||
"github.com/go-openapi/runtime/client"
|
||||
"github.com/gorilla/sessions"
|
||||
"github.com/knadh/koanf"
|
||||
jsonParser "github.com/knadh/koanf/parsers/json"
|
||||
"github.com/knadh/koanf/providers/confmap"
|
||||
"github.com/knadh/koanf/providers/env"
|
||||
"github.com/knadh/koanf/providers/file"
|
||||
"github.com/lestrrat-go/jwx/jwk"
|
||||
"github.com/lestrrat-go/jwx/jwt"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"golang.org/x/oauth2"
|
||||
)
|
||||
|
||||
var oauth2Config *oauth2.Config
|
||||
|
||||
type OpenIDConfiguration struct {
|
||||
AuthorizationEndpoint string `json:"authorization_endpoint"`
|
||||
TokenEndpoint string `json:"token_endpoint"`
|
||||
JWKSUri string `json:"jwks_uri"`
|
||||
JwksUri string `json:"jwks_uri"`
|
||||
EndSessionEndpoint string `json:"end_session_endpoint"`
|
||||
}
|
||||
|
||||
var (
|
||||
sessionStore *sessions.FilesystemStore
|
||||
k = koanf.New(".")
|
||||
)
|
||||
|
||||
const (
|
||||
sessionKeyAccessToken = iota
|
||||
sessionKeyRefreshToken
|
||||
sessionKeyIdToken
|
||||
sessionKeyUserId
|
||||
sessionKeyRoles
|
||||
sessionKeyEmail
|
||||
sessionKeyUsername
|
||||
sessionRedirectTarget
|
||||
)
|
||||
|
||||
func main() {
|
||||
headers := map[string][]string{
|
||||
"Accept": {"application/json"},
|
||||
if err := k.Load(file.Provider("resourceapp.json"), jsonParser.Parser()); err != nil && !os.IsNotExist(err) {
|
||||
log.Fatalf("error loading config: %v", err)
|
||||
}
|
||||
const prefix = "RESOURCEAPP_"
|
||||
if err := k.Load(env.Provider(prefix, ".", func(s string) string {
|
||||
return strings.Replace(strings.ToLower(
|
||||
strings.TrimPrefix(s, prefix)), "_", ".", -1)
|
||||
}), nil); err != nil {
|
||||
log.Fatalf("error loading config: %v", err)
|
||||
}
|
||||
|
||||
var body []byte
|
||||
oidcServer := k.MustString("oidc.server")
|
||||
oidcClientId := k.MustString("oidc.client-id")
|
||||
oidcClientSecret := k.MustString("oidc.client-secret")
|
||||
|
||||
req, err := http.NewRequest(http.MethodGet, "https://localhost:4444/.well-known/openid-configuration", bytes.NewBuffer(body))
|
||||
sessionPath := k.MustString("session.path")
|
||||
sessionAuthKey, err := base64.StdEncoding.DecodeString(k.String("session.auth-key"))
|
||||
if err != nil {
|
||||
log.Panic(err)
|
||||
log.Fatalf("could not decode session auth key: %s", err)
|
||||
}
|
||||
req.Header = headers
|
||||
|
||||
client, err := client2.TLSClient(client2.TLSClientOptions{InsecureSkipVerify: true})
|
||||
sessionEncKey, err := base64.StdEncoding.DecodeString(k.String("session.enc-key"))
|
||||
if err != nil {
|
||||
log.Panic(err)
|
||||
}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
log.Panic(err)
|
||||
log.Fatalf("could not decode session encryption key: %s", err)
|
||||
}
|
||||
|
||||
dec := json.NewDecoder(resp.Body)
|
||||
discoveryResponse := &OpenIDConfiguration{}
|
||||
err = dec.Decode(discoveryResponse)
|
||||
if err != nil {
|
||||
log.Panic(err)
|
||||
generated := false
|
||||
if len(sessionAuthKey) != 64 {
|
||||
sessionAuthKey = generateKey(64)
|
||||
generated = true
|
||||
}
|
||||
if len(sessionEncKey) != 32 {
|
||||
sessionEncKey = generateKey(32)
|
||||
generated = true
|
||||
}
|
||||
|
||||
oauth2Config = &oauth2.Config{
|
||||
ClientID: "local-test-app",
|
||||
ClientSecret: "uzvTqaCvUSBMd0aVjECmD-egAJ",
|
||||
if generated {
|
||||
_ = k.Load(confmap.Provider(map[string]interface{}{
|
||||
"session.auth-key": sessionAuthKey,
|
||||
"session.enc-key": sessionEncKey,
|
||||
}, "."), nil)
|
||||
jsonData, err := k.Marshal(jsonParser.Parser())
|
||||
if err != nil {
|
||||
log.Fatalf("could not encode session config")
|
||||
}
|
||||
log.Infof("put the following in your resourceapp.json:\n%s", string(jsonData))
|
||||
}
|
||||
|
||||
var discoveryResponse OpenIDConfiguration
|
||||
var discoveryUrl *url.URL
|
||||
|
||||
if discoveryUrl, err = url.Parse(oidcServer); err != nil {
|
||||
log.Fatalf("could not parse oidc.server parameter value %s: %s", oidcServer, err)
|
||||
} else {
|
||||
discoveryUrl.Path = "/.well-known/openid-configuration"
|
||||
}
|
||||
apiClient, err := client.TLSClient(client.TLSClientOptions{InsecureSkipVerify: true})
|
||||
if err := discoverOidc(discoveryUrl, apiClient, &discoveryResponse); err != nil {
|
||||
log.Fatalf("OpenID Connect discovery failed: %s", err)
|
||||
}
|
||||
oauth2Config := &oauth2.Config{
|
||||
ClientID: oidcClientId,
|
||||
ClientSecret: oidcClientSecret,
|
||||
Endpoint: oauth2.Endpoint{
|
||||
AuthURL: discoveryResponse.AuthorizationEndpoint,
|
||||
TokenURL: discoveryResponse.TokenEndpoint,
|
||||
},
|
||||
Scopes: []string{"openid", "offline"},
|
||||
}
|
||||
keySet, err := jwk.FetchHTTP(discoveryResponse.JwksUri, jwk.WithHTTPClient(apiClient))
|
||||
if err != nil {
|
||||
log.Fatalf("could not fetch JWKs: %s", err)
|
||||
}
|
||||
|
||||
http.Handle("/", NewIndexPage())
|
||||
http.Handle("/callback", NewCallbackHandler(discoveryResponse.JWKSUri))
|
||||
if _, err = os.Stat(sessionPath); err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
if err = os.MkdirAll(sessionPath, 0700); err != nil {
|
||||
log.Fatalf("could not create session store directory: %s", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
sessionStore = sessions.NewFilesystemStore(sessionPath, sessionAuthKey, sessionEncKey)
|
||||
|
||||
http.Handle("/", authenticate(oauth2Config)(NewIndexPage(discoveryResponse.EndSessionEndpoint)))
|
||||
http.Handle("/callback", NewCallbackHandler(keySet, oauth2Config))
|
||||
|
||||
err = http.ListenAndServe(":4000", http.DefaultServeMux)
|
||||
if err != nil {
|
||||
log.Panic(err)
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func generateKey(length int) []byte {
|
||||
key := make([]byte, length)
|
||||
read, err := rand.Read(key)
|
||||
if err != nil {
|
||||
log.Fatalf("could not generate key: %s", err)
|
||||
}
|
||||
if read != length {
|
||||
log.Fatalf("read %d bytes, expected %d bytes", read, length)
|
||||
}
|
||||
return key
|
||||
}
|
||||
|
||||
func discoverOidc(discoveryUrl *url.URL, apiClient *http.Client, o *OpenIDConfiguration) error {
|
||||
var body []byte
|
||||
req, err := http.NewRequest(http.MethodGet, discoveryUrl.String(), bytes.NewBuffer(body))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
req.Header = map[string][]string{
|
||||
"Accept": {"application/json"},
|
||||
}
|
||||
|
||||
resp, err := apiClient.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
dec := json.NewDecoder(resp.Body)
|
||||
err = dec.Decode(o)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type callbackHandler struct {
|
||||
JwksUri string
|
||||
keySet *jwk.Set
|
||||
oauth2Config *oauth2.Config
|
||||
}
|
||||
|
||||
func (c *callbackHandler) ServeHTTP(writer http.ResponseWriter, request *http.Request) {
|
||||
|
@ -86,55 +192,121 @@ func (c *callbackHandler) ServeHTTP(writer http.ResponseWriter, request *http.Re
|
|||
}
|
||||
|
||||
code := request.URL.Query().Get("code")
|
||||
scope := request.URL.Query().Get("scope")
|
||||
state := request.URL.Query().Get("state")
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
httpClient, err := client2.TLSClient(client2.TLSClientOptions{InsecureSkipVerify: true})
|
||||
httpClient, err := client.TLSClient(client.TLSClientOptions{InsecureSkipVerify: true})
|
||||
ctx = context.WithValue(ctx, oauth2.HTTPClient, httpClient)
|
||||
|
||||
tok, err := oauth2Config.Exchange(ctx, code)
|
||||
tok, err := c.oauth2Config.Exchange(ctx, code)
|
||||
if err != nil {
|
||||
log.Print(err)
|
||||
log.Error(err)
|
||||
http.Error(writer, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
accessToken := tok.AccessToken
|
||||
refreshToken := tok.RefreshToken
|
||||
session, err := sessionStore.Get(request, "resource_session")
|
||||
if err != nil {
|
||||
http.Error(writer, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
session.Values[sessionKeyAccessToken] = tok.AccessToken
|
||||
session.Values[sessionKeyRefreshToken] = tok.RefreshToken
|
||||
session.Values[sessionKeyIdToken] = tok.Extra("id_token").(string)
|
||||
|
||||
idToken := tok.Extra("id_token")
|
||||
|
||||
keySet, err := jwk.FetchHTTP(c.JwksUri, jwk.WithHTTPClient(httpClient))
|
||||
if err != nil {
|
||||
log.Print(err)
|
||||
if parsedIdToken, err := jwt.ParseString(idToken.(string), jwt.WithKeySet(c.keySet), jwt.WithOpenIDClaims()); err != nil {
|
||||
log.Error(err)
|
||||
http.Error(writer, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||
return
|
||||
} else {
|
||||
log.Infof(`
|
||||
ID Token
|
||||
========
|
||||
|
||||
Subject: %s
|
||||
Audience: %s
|
||||
Issued at: %s
|
||||
Issued by: %s
|
||||
Not valid before: %s
|
||||
Not valid after: %s
|
||||
|
||||
`,
|
||||
parsedIdToken.Subject(),
|
||||
parsedIdToken.Audience(),
|
||||
parsedIdToken.IssuedAt(),
|
||||
parsedIdToken.Issuer(),
|
||||
parsedIdToken.NotBefore(),
|
||||
parsedIdToken.Expiration(),
|
||||
)
|
||||
|
||||
session.Values[sessionKeyUserId] = parsedIdToken.Subject()
|
||||
|
||||
if roles, ok := parsedIdToken.Get("Groups"); ok {
|
||||
session.Values[sessionKeyRoles] = roles
|
||||
}
|
||||
parsedIdToken, err := jwt.Parse(strings.NewReader(idToken.(string)), jwt.WithKeySet(keySet))
|
||||
if username, ok := parsedIdToken.Get("Username"); ok {
|
||||
session.Values[sessionKeyUsername] = username
|
||||
}
|
||||
if email, ok := parsedIdToken.Get("Email"); ok {
|
||||
session.Values[sessionKeyEmail] = email
|
||||
}
|
||||
}
|
||||
if err = session.Save(request, writer); err != nil {
|
||||
http.Error(writer, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
if redirectTarget, ok := session.Values[sessionRedirectTarget]; ok {
|
||||
writer.Header().Set("Location", redirectTarget.(string))
|
||||
} else {
|
||||
writer.Header().Set("Location", "/")
|
||||
}
|
||||
|
||||
writer.WriteHeader(http.StatusFound)
|
||||
}
|
||||
|
||||
func NewCallbackHandler(keySet *jwk.Set, oauth2Config *oauth2.Config) *callbackHandler {
|
||||
return &callbackHandler{keySet: keySet, oauth2Config: oauth2Config}
|
||||
}
|
||||
|
||||
func authenticate(oauth2Config *oauth2.Config) func(http.Handler) http.Handler {
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
session, err := sessionStore.Get(r, "resource_session")
|
||||
if err != nil {
|
||||
log.Print(err)
|
||||
http.Error(writer, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
_, err = fmt.Fprintf(
|
||||
writer,
|
||||
"scope: %s, state: %s\ncode %s -> token %+v\n\naccess: %+v\nrefresh: %+v\nid: %+v\n\nParsed id token:\n%+v",
|
||||
code, scope, state, tok, accessToken, refreshToken, idToken, parsedIdToken)
|
||||
if err != nil {
|
||||
log.Print(err)
|
||||
http.Error(writer, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||
if _, ok := session.Values[sessionKeyUserId]; ok {
|
||||
next.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
session.Values[sessionRedirectTarget] = r.URL.String()
|
||||
if err = session.Save(r, w); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
var authUrl *url.URL
|
||||
if authUrl, err = url.Parse(oauth2Config.Endpoint.AuthURL); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
queryValues := authUrl.Query()
|
||||
queryValues.Set("client_id", k.String("oidc.client-id"))
|
||||
queryValues.Set("response_type", "code")
|
||||
queryValues.Set("scope", "openid offline")
|
||||
queryValues.Set("state", base64.URLEncoding.EncodeToString(generateKey(8)))
|
||||
authUrl.RawQuery = queryValues.Encode()
|
||||
|
||||
w.Header().Set("Location", authUrl.String())
|
||||
w.WriteHeader(http.StatusFound)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func NewCallbackHandler(jwksUri string) *callbackHandler {
|
||||
return &callbackHandler{JwksUri: jwksUri}
|
||||
type indexHandler struct {
|
||||
logoutUrl string
|
||||
}
|
||||
|
||||
type indexHandler struct{}
|
||||
|
||||
func (i indexHandler) ServeHTTP(writer http.ResponseWriter, request *http.Request) {
|
||||
if request.Method != http.MethodGet {
|
||||
http.Error(writer, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed)
|
||||
|
@ -145,22 +317,56 @@ func (i indexHandler) ServeHTTP(writer http.ResponseWriter, request *http.Reques
|
|||
return
|
||||
}
|
||||
writer.WriteHeader(http.StatusOK)
|
||||
writer.Header().Add("Content-Type", "text/html")
|
||||
_, err := writer.Write([]byte(`
|
||||
|
||||
page, err := template.New("").Parse(`
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head><title>Auth test</title></head>
|
||||
<body>
|
||||
<h1>Hello World</h1>
|
||||
<a href="https://localhost:4444/oauth2/auth?client_id=local-test-app&response_type=code&scope=openid%20offline&state=12345678">Login</a>
|
||||
<h1>Hello {{ .User }}</h1>
|
||||
<p>This is an authorization protected resource</p>
|
||||
<a href="{{ .LogoutURL }}">Logout</a>
|
||||
</body>
|
||||
</html>
|
||||
`))
|
||||
`)
|
||||
if err != nil {
|
||||
log.Panic(err)
|
||||
http.Error(writer, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
session, err := sessionStore.Get(request, "resource_session")
|
||||
if err != nil {
|
||||
http.Error(writer, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
logoutUrl, err := url.Parse(i.logoutUrl)
|
||||
if err != nil {
|
||||
http.Error(writer, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
var user string
|
||||
var ok bool
|
||||
if user, ok = session.Values[sessionKeyUsername].(string); ok {
|
||||
|
||||
}
|
||||
if idToken, ok := session.Values[sessionKeyIdToken].(string); ok {
|
||||
logoutUrl.RawQuery = url.Values{
|
||||
"id_token_hint": []string{idToken},
|
||||
"post_logout_redirect_uri": []string{"/logged_out"},
|
||||
}.Encode()
|
||||
}
|
||||
|
||||
writer.Header().Add("Content-Type", "text/html")
|
||||
err = page.Execute(writer, map[string]interface{}{
|
||||
"User": user,
|
||||
"LogoutURL": logoutUrl.String(),
|
||||
})
|
||||
if err != nil {
|
||||
http.Error(writer, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func NewIndexPage() *indexHandler {
|
||||
return &indexHandler{}
|
||||
func NewIndexPage(logoutUrl string) *indexHandler {
|
||||
return &indexHandler{logoutUrl: logoutUrl}
|
||||
}
|
||||
|
|
10
go.mod
10
go.mod
|
@ -3,14 +3,22 @@ module git.cacert.org/oidc_login
|
|||
go 1.14
|
||||
|
||||
require (
|
||||
github.com/BurntSushi/toml v0.3.1
|
||||
github.com/go-openapi/runtime v0.19.22
|
||||
github.com/go-playground/form/v4 v4.1.1
|
||||
github.com/go-playground/validator/v10 v10.4.1
|
||||
github.com/golang/protobuf v1.4.0 // indirect
|
||||
github.com/gorilla/csrf v1.7.0
|
||||
github.com/gorilla/sessions v1.2.1
|
||||
github.com/knadh/koanf v0.14.0
|
||||
github.com/lestrrat-go/jwx v1.0.6
|
||||
github.com/nicksnyder/go-i18n/v2 v2.1.1
|
||||
github.com/ory/hydra-client-go v1.8.5
|
||||
github.com/sirupsen/logrus v1.4.2
|
||||
github.com/tidwall/pretty v1.0.1 // indirect
|
||||
golang.org/x/crypto v0.0.0-20200709230013-948cd5f35899 // indirect
|
||||
golang.org/x/net v0.0.0-20200625001655-4c5254603344 // indirect
|
||||
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d
|
||||
golang.org/x/text v0.3.3 // indirect
|
||||
golang.org/x/text v0.3.3
|
||||
google.golang.org/appengine v1.6.5 // indirect
|
||||
)
|
||||
|
|
40
go.sum
40
go.sum
|
@ -1,4 +1,5 @@
|
|||
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/PuerkitoBio/purell v1.1.0/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=
|
||||
github.com/PuerkitoBio/purell v1.1.1 h1:WEQqlqaGbrPkxLJWfBwQmfEAE1Z7ONdDLqrN38tNFfI=
|
||||
|
@ -19,6 +20,9 @@ github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs
|
|||
github.com/docker/go-units v0.3.3/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
|
||||
github.com/docker/go-units v0.4.0 h1:3uh0PgVws3nIA0Q+MwDC8yjEPf9zjRfZZWXZYDct3Tw=
|
||||
github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
|
||||
github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M=
|
||||
github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4=
|
||||
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
|
||||
github.com/globalsign/mgo v0.0.0-20180905125535-1ca0a4f7cbcb/go.mod h1:xkRDCp4j0OGD1HRkm4kmhM+pmpv3AKq5SU7GMg4oO/Q=
|
||||
github.com/globalsign/mgo v0.0.0-20181015135952-eeefdecb41b8/go.mod h1:xkRDCp4j0OGD1HRkm4kmhM+pmpv3AKq5SU7GMg4oO/Q=
|
||||
github.com/go-openapi/analysis v0.0.0-20180825180245-b006789cd277/go.mod h1:k70tL6pCuVxPJOHXQ+wIac1FUrvNkHolPie/cLEU6hI=
|
||||
|
@ -88,6 +92,16 @@ github.com/go-openapi/validate v0.19.3/go.mod h1:90Vh6jjkTn+OT1Eefm0ZixWNFjhtOH7
|
|||
github.com/go-openapi/validate v0.19.10/go.mod h1:RKEZTUWDkxKQxN2jDT7ZnZi2bhZlbNMAuKvKB+IaGx8=
|
||||
github.com/go-openapi/validate v0.19.11 h1:8lCr0b9lNWKjVjW/hSZZvltUy+bULl7vbnCTsOzlhPo=
|
||||
github.com/go-openapi/validate v0.19.11/go.mod h1:Rzou8hA/CBw8donlS6WNEUQupNvUZ0waH08tGe6kAQ4=
|
||||
github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A=
|
||||
github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
||||
github.com/go-playground/form/v4 v4.1.1 h1:1T9lGt3WRHuDwT5uwx7RYQZfxVwWZhF0DC1ovKyNnWY=
|
||||
github.com/go-playground/form/v4 v4.1.1/go.mod h1:q1a2BY+AQUUzhl6xA/6hBetay6dEIhMHjgvJiGo6K7U=
|
||||
github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q=
|
||||
github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8=
|
||||
github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no=
|
||||
github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA=
|
||||
github.com/go-playground/validator/v10 v10.4.1 h1:pH2c5ADXtd66mxoE0Zm9SUhxE20r7aM3F26W0hOn+GE=
|
||||
github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4=
|
||||
github.com/go-stack/stack v1.8.0 h1:5SgMzNM5HxrEjV0ww2lTmX6E2Izsfxas4+YHWRs3Lsk=
|
||||
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
|
||||
github.com/gobuffalo/attrs v0.0.0-20190224210810-a9411de4debd/go.mod h1:4duuawTqi2wkkpB4ePgWMaai6/Kc6WEz83bhFwpHzj0=
|
||||
|
@ -131,6 +145,14 @@ github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
|
|||
github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY=
|
||||
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/gorilla/csrf v1.7.0 h1:mMPjV5/3Zd460xCavIkppUdvnl5fPXMpv2uz2Zyg7/Y=
|
||||
github.com/gorilla/csrf v1.7.0/go.mod h1:+a/4tCmqhG6/w4oafeAZ9pEa3/NZOWYVbD9fV0FwIQA=
|
||||
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/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
|
||||
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
|
||||
github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=
|
||||
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
|
||||
github.com/joho/godotenv v1.3.0 h1:Zjp+RcGpHhGlrMbJzXTrZZPrWj+1vfm90La1wgB6Bhc=
|
||||
|
@ -139,7 +161,10 @@ github.com/karrick/godirwalk v1.8.0/go.mod h1:H5KPZjojv4lE+QYImBI8xVtrBRgYrIVsaR
|
|||
github.com/karrick/godirwalk v1.10.3/go.mod h1:RoGL9dQei4vP9ilrpETWE8CLOZ1kiN0LhBygSwrAsHA=
|
||||
github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00=
|
||||
github.com/klauspost/compress v1.9.5/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
|
||||
github.com/knadh/koanf v0.14.0 h1:h9XeG4wEiEuxdxqv/SbY7TEK+7vzrg/dOaGB+S6+mPo=
|
||||
github.com/knadh/koanf v0.14.0/go.mod h1:H5mEFsTeWizwFXHKtsITL5ipsLTuAMQoGuQpp+1JL9U=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.2 h1:DB17ag19krx9CFsz4o3enTrPXyIXCl+2iCXH/aMAp9s=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
|
@ -147,6 +172,8 @@ github.com/kr/pty v1.1.5/go.mod h1:9r2w37qlBe7rQ6e1fg1S/9xpWHSnaqNdHD3WcMdbPDA=
|
|||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y=
|
||||
github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII=
|
||||
github.com/lestrrat-go/iter v0.0.0-20200422075355-fc1769541911 h1:FvnrqecqX4zT0wOIbYK1gNgTm0677INEWiFY8UEYggY=
|
||||
github.com/lestrrat-go/iter v0.0.0-20200422075355-fc1769541911/go.mod h1:zIdgO1mRKhn8l9vrZJZz9TUMMFbQbLeTsbqPDrJ/OJc=
|
||||
github.com/lestrrat-go/jwx v1.0.6 h1:0absmJ/XlsxNkXr9syeIHjCJnu3rZa+DKzdCI6QfYgU=
|
||||
|
@ -162,9 +189,12 @@ github.com/mailru/easyjson v0.7.1/go.mod h1:KAzv3t3aY1NaHWoQz1+4F1ccyAH66Jk7yos7
|
|||
github.com/markbates/oncer v0.0.0-20181203154359-bf2de49a0be2/go.mod h1:Ld9puTsIW75CHf65OeIOkyKbteujpZVXDpWK6YGZbxE=
|
||||
github.com/markbates/safe v1.0.1/go.mod h1:nAqgmRi7cY2nqMc92/bSEeQA+R4OheNU2T1kNSCBdG0=
|
||||
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
|
||||
github.com/mitchellh/mapstructure v1.2.2/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
|
||||
github.com/mitchellh/mapstructure v1.3.2 h1:mRS76wmkOn3KkKAyXDu42V+6ebnXWIztFSYGN7GeoRg=
|
||||
github.com/mitchellh/mapstructure v1.3.2/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
|
||||
github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc=
|
||||
github.com/nicksnyder/go-i18n/v2 v2.1.1 h1:ATCOanRDlrfKVB4WHAdJnLEqZtDmKYsweqsOUYflnBU=
|
||||
github.com/nicksnyder/go-i18n/v2 v2.1.1/go.mod h1:d++QJC9ZVf7pa48qrsRWhMJ5pSHIPmS3OLqK1niyLxs=
|
||||
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs=
|
||||
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
|
||||
github.com/ory/hydra-client-go v1.8.5 h1:YyoI07CnL1kikTElbSEg9EsZyT0mtl+2fiXLgwvs7XU=
|
||||
|
@ -172,21 +202,27 @@ github.com/ory/hydra-client-go v1.8.5/go.mod h1:85NsQeF1Lof8B77fJjEf6J8mbafW5Wxl
|
|||
github.com/pborman/uuid v1.2.0 h1:J7Q5mO4ysT1dv8hyrUGHb9+ooztCXu1D8MY8DZYsu3g=
|
||||
github.com/pborman/uuid v1.2.0/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k=
|
||||
github.com/pelletier/go-toml v1.4.0/go.mod h1:PN7xzY2wHTK0K9p34ErDQMlFxa51Fk0OUruD3k1mMwo=
|
||||
github.com/pelletier/go-toml v1.7.0 h1:7utD74fnzVc/cpcyy8sjrlFr5vYpypUixARcHIMIGuI=
|
||||
github.com/pelletier/go-toml v1.7.0/go.mod h1:vwGMzjaWMwyfHwgIBhI2YUM4fB6nL6lVAvS1LBMMhTE=
|
||||
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.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/rhnvrm/simples3 v0.5.0/go.mod h1:Y+3vYm2V7Y4VijFoJHHTrja6OgPrJ2cBti8dPGkC3sA=
|
||||
github.com/rogpeppe/go-internal v1.1.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
||||
github.com/rogpeppe/go-internal v1.2.2/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
||||
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
||||
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
|
||||
github.com/sirupsen/logrus v1.4.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
|
||||
github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q=
|
||||
github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4=
|
||||
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
|
||||
github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ=
|
||||
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
|
||||
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
|
||||
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
|
||||
|
@ -251,7 +287,11 @@ golang.org/x/sys v0.0.0-20190419153524-e8e3143a4f4a/go.mod h1:h1NjWce9XRLGQEsW7w
|
|||
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190531175056-4c3a928424d2/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190616124812-15dcb6c0061f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd h1:xhmwyvizuTgC2qz7ZlMluP20uW+C3Rm0FD/WLDX8884=
|
||||
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200331124033-c3d80250170d h1:nc5K6ox/4lTFbMVSL9WRR81ixkcwXThoiF6yf+R9scA=
|
||||
golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
|
|
277
main.go
277
main.go
|
@ -1,37 +1,188 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"log"
|
||||
"context"
|
||||
"flag"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"os/signal"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/BurntSushi/toml"
|
||||
openApiClient "github.com/go-openapi/runtime/client"
|
||||
"github.com/go-playground/form/v4"
|
||||
"github.com/go-playground/validator/v10"
|
||||
"github.com/gorilla/csrf"
|
||||
"github.com/nicksnyder/go-i18n/v2/i18n"
|
||||
"github.com/ory/hydra-client-go/client"
|
||||
"github.com/ory/hydra-client-go/client/admin"
|
||||
"github.com/ory/hydra-client-go/models"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"golang.org/x/text/language"
|
||||
)
|
||||
|
||||
var adminClient *client.OryHydra
|
||||
type key int
|
||||
|
||||
const (
|
||||
requestIdKey key = iota
|
||||
)
|
||||
|
||||
var (
|
||||
adminClient *client.OryHydra
|
||||
listenAddr string
|
||||
healthy int32
|
||||
validate *validator.Validate
|
||||
bundle *i18n.Bundle
|
||||
messageCatalog map[string]*i18n.Message
|
||||
)
|
||||
|
||||
func main() {
|
||||
flag.StringVar(&listenAddr, "listen-addr", ":3000", "server listen address")
|
||||
flag.Parse()
|
||||
|
||||
logger := log.New()
|
||||
logger.Infoln("Server is starting")
|
||||
|
||||
validate = validator.New()
|
||||
|
||||
router := http.NewServeMux()
|
||||
loginHandler, err := NewLoginHandler()
|
||||
router.Handle("/login", loginHandler)
|
||||
router.Handle("/consent", NewConsentHandler())
|
||||
router.Handle("/health", health())
|
||||
|
||||
adminURL, err := url.Parse("https://localhost:4445/")
|
||||
if err != nil {
|
||||
log.Panic(err)
|
||||
}
|
||||
apiclient, err := openApiClient.TLSClient(openApiClient.TLSClientOptions{InsecureSkipVerify: true})
|
||||
apiClient, err := openApiClient.TLSClient(openApiClient.TLSClientOptions{InsecureSkipVerify: true})
|
||||
if err != nil {
|
||||
log.Panic(err)
|
||||
}
|
||||
clientTransport := openApiClient.NewWithClient(adminURL.Host, adminURL.Path, []string{adminURL.Scheme}, apiclient)
|
||||
clientTransport := openApiClient.NewWithClient(adminURL.Host, adminURL.Path, []string{adminURL.Scheme}, apiClient)
|
||||
adminClient = client.New(clientTransport, nil)
|
||||
|
||||
http.Handle("/login", NewLoginHandler())
|
||||
http.Handle("/consent", NewConsentHandler())
|
||||
|
||||
err = http.ListenAndServe(":3000", http.DefaultServeMux)
|
||||
if err != nil {
|
||||
log.Panic(err)
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
csrfKey := []byte("abcdefghijklmnopqrstuvwxyz012345")
|
||||
handler := csrf.Protect(csrfKey)(router)
|
||||
|
||||
nextRequestId := func() string {
|
||||
return fmt.Sprintf("%d", time.Now().UnixNano())
|
||||
}
|
||||
|
||||
bundle = i18n.NewBundle(language.English)
|
||||
bundle.RegisterUnmarshalFunc("toml", toml.Unmarshal)
|
||||
_, err = bundle.LoadMessageFile("de.toml")
|
||||
if err != nil {
|
||||
logger.Warnln("message bundle de.toml not found")
|
||||
}
|
||||
|
||||
initMessageCatalog()
|
||||
|
||||
server := &http.Server{
|
||||
Addr: ":3000",
|
||||
Handler: tracing(nextRequestId)(logging(logger)(handler)),
|
||||
ReadTimeout: 5 * time.Second,
|
||||
WriteTimeout: 10 * time.Second,
|
||||
IdleTimeout: 15 * time.Second,
|
||||
}
|
||||
|
||||
done := make(chan bool)
|
||||
quit := make(chan os.Signal, 1)
|
||||
signal.Notify(quit, os.Interrupt)
|
||||
|
||||
go func() {
|
||||
<-quit
|
||||
logger.Infoln("Server is shutting down...")
|
||||
atomic.StoreInt32(&healthy, 0)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer cancel()
|
||||
|
||||
server.SetKeepAlivesEnabled(false)
|
||||
if err := server.Shutdown(ctx); err != nil {
|
||||
logger.Fatalf("Could not gracefully shutdown the server: %v\n", err)
|
||||
}
|
||||
close(done)
|
||||
}()
|
||||
|
||||
logger.Infoln("Server is ready to handle requests at", listenAddr)
|
||||
atomic.StoreInt32(&healthy, 1)
|
||||
if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
|
||||
logger.Fatalf("Could not listen on %s: %v\n", listenAddr, err)
|
||||
}
|
||||
|
||||
<-done
|
||||
logger.Infoln("Server stopped")
|
||||
}
|
||||
|
||||
func initMessageCatalog() {
|
||||
messageCatalog = make(map[string]*i18n.Message)
|
||||
messageCatalog["unknown"] = &i18n.Message{
|
||||
ID: "ErrorUnknown",
|
||||
Other: "Unknown error",
|
||||
}
|
||||
messageCatalog["email"] = &i18n.Message{
|
||||
ID: "ErrorEmail",
|
||||
Other: "Please enter a valid email address.",
|
||||
}
|
||||
messageCatalog["Email-required"] = &i18n.Message{
|
||||
ID: "ErrorEmailRequired",
|
||||
Other: "Please enter an email address.",
|
||||
}
|
||||
messageCatalog["required"] = &i18n.Message{
|
||||
ID: "ErrorRequired",
|
||||
Other: "Please enter a value",
|
||||
}
|
||||
messageCatalog["Password-required"] = &i18n.Message{
|
||||
ID: "ErrorPasswordRequired",
|
||||
Other: "Please enter a password.",
|
||||
}
|
||||
}
|
||||
|
||||
func health() http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if atomic.LoadInt32(&healthy) == 1 {
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
return
|
||||
}
|
||||
w.WriteHeader(http.StatusServiceUnavailable)
|
||||
})
|
||||
}
|
||||
|
||||
func logging(logger *log.Logger) func(http.Handler) http.Handler {
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
defer func() {
|
||||
requestId, ok := r.Context().Value(requestIdKey).(string)
|
||||
if !ok {
|
||||
requestId = "unknown"
|
||||
}
|
||||
logger.Infoln(requestId, r.Method, r.URL.Path, r.RemoteAddr, r.UserAgent())
|
||||
}()
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func tracing(nextRequestId func() string) func(http.Handler) http.Handler {
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
requestId := r.Header.Get("X-Request-Id")
|
||||
if requestId == "" {
|
||||
requestId = nextRequestId()
|
||||
}
|
||||
ctx := context.WithValue(r.Context(), requestIdKey, requestId)
|
||||
w.Header().Set("X-Request-Id", requestId)
|
||||
next.ServeHTTP(w, r.WithContext(ctx))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -59,18 +210,80 @@ func NewConsentHandler() *consentHandler {
|
|||
}
|
||||
|
||||
type loginHandler struct {
|
||||
loginTemplate *template.Template
|
||||
}
|
||||
|
||||
type LoginInformation struct {
|
||||
Email string `form:"email" validate:"required,email"`
|
||||
Password string `form:"password" validate:"required"`
|
||||
}
|
||||
|
||||
func (l *loginHandler) ServeHTTP(writer http.ResponseWriter, request *http.Request) {
|
||||
var err error
|
||||
challenge := request.URL.Query().Get("login_challenge")
|
||||
log.Printf("received challenge %s\n", challenge)
|
||||
log.Debugf("received challenge %s\n", challenge)
|
||||
|
||||
switch request.Method {
|
||||
case http.MethodGet:
|
||||
// GET should render login form
|
||||
|
||||
err = l.loginTemplate.Lookup("base").Execute(writer, map[string]interface{}{
|
||||
"Title": "Title",
|
||||
csrf.TemplateTag: csrf.TemplateField(request),
|
||||
"LabelEmail": "Email",
|
||||
"LabelPassword": "Password",
|
||||
"LabelLogin": "Login",
|
||||
"errors": map[string]string{},
|
||||
})
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
http.Error(writer, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
break
|
||||
case http.MethodPost:
|
||||
// POST should perform the action
|
||||
var loginInfo LoginInformation
|
||||
|
||||
// validate input
|
||||
decoder := form.NewDecoder()
|
||||
err = decoder.Decode(&loginInfo, request.Form)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
http.Error(writer, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
err := validate.Struct(&loginInfo)
|
||||
if err != nil {
|
||||
errors := make(map[string]string)
|
||||
for _, err := range err.(validator.ValidationErrors) {
|
||||
accept := request.Header.Get("Accept-Language")
|
||||
errors[err.Field()] = lookupErrorMessage(err.Tag(), err.Field(), err.Value(), i18n.NewLocalizer(bundle, accept))
|
||||
}
|
||||
|
||||
err = l.loginTemplate.Lookup("base").Execute(writer, map[string]interface{}{
|
||||
"Title": "Title",
|
||||
csrf.TemplateTag: csrf.TemplateField(request),
|
||||
"LabelEmail": "Email",
|
||||
"LabelPassword": "Password",
|
||||
"LabelLogin": "Login",
|
||||
"Email": loginInfo.Email,
|
||||
"errors": errors,
|
||||
})
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
http.Error(writer, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// GET user data
|
||||
// finish login and redirect to target
|
||||
// TODO: get or generate a user id
|
||||
subject := "a-user-with-an-id"
|
||||
loginRequest, err := adminClient.Admin.AcceptLoginRequest(admin.NewAcceptLoginRequestParams().WithLoginChallenge(challenge).WithBody(&models.AcceptLoginRequest{
|
||||
loginRequest, err := adminClient.Admin.AcceptLoginRequest(
|
||||
admin.NewAcceptLoginRequestParams().WithLoginChallenge(challenge).WithBody(&models.AcceptLoginRequest{
|
||||
Acr: "no-creds",
|
||||
Remember: true,
|
||||
RememberFor: 0,
|
||||
|
@ -81,8 +294,46 @@ func (l *loginHandler) ServeHTTP(writer http.ResponseWriter, request *http.Reque
|
|||
}
|
||||
writer.Header().Add("Location", *loginRequest.GetPayload().RedirectTo)
|
||||
writer.WriteHeader(http.StatusFound)
|
||||
break
|
||||
default:
|
||||
http.Error(writer, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func NewLoginHandler() *loginHandler {
|
||||
return &loginHandler{}
|
||||
func lookupErrorMessage(tag string, field string, value interface{}, l *i18n.Localizer) string {
|
||||
var message *i18n.Message
|
||||
message, ok := messageCatalog[fmt.Sprintf("%s-%s", field, tag)]
|
||||
if !ok {
|
||||
log.Infof("no specific error message %s-%s", field, tag)
|
||||
message, ok = messageCatalog[tag]
|
||||
if !ok {
|
||||
log.Infof("no specific error message %s", tag)
|
||||
message, ok = messageCatalog["unknown"]
|
||||
if !ok {
|
||||
log.Error("no default translation found")
|
||||
return tag
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
translation, err := l.Localize(&i18n.LocalizeConfig{
|
||||
DefaultMessage: message,
|
||||
TemplateData: map[string]interface{}{
|
||||
"Value": value,
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
return tag
|
||||
}
|
||||
return translation
|
||||
}
|
||||
|
||||
func NewLoginHandler() (*loginHandler, error) {
|
||||
loginTemplate, err := template.ParseFiles("templates/base.html", "templates/login.html")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &loginHandler{loginTemplate: loginTemplate}, nil
|
||||
}
|
||||
|
|
10
templates/base.html
Normal file
10
templates/base.html
Normal file
|
@ -0,0 +1,10 @@
|
|||
{{ define "base" }}
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head><title>{{ .Title }}</title></head>
|
||||
<body>
|
||||
<h1>{{ .Title }}</h1>
|
||||
{{ template "content" . }}
|
||||
</body>
|
||||
</html>
|
||||
{{ end }}
|
12
templates/login.html
Normal file
12
templates/login.html
Normal file
|
@ -0,0 +1,12 @@
|
|||
{{ define "content" }}
|
||||
<form method="post">
|
||||
{{ .csrfField }}
|
||||
{{ if .errors.Email }}<p>{{ .errors.Email }}</p>{{ end }}
|
||||
<label for="email">{{ .LabelEmail }}</label>
|
||||
<input type="text" id="email" name="email" value="{{ .Email }}"/><br/>
|
||||
{{ if .errors.Password }}<p>{{ .errors.Password }}</p>{{ end }}
|
||||
<label for="password">{{ .LabelPassword }}</label>
|
||||
<input type="password" id="password" name="password" value=""/><br/>
|
||||
<button type="submit">{{ .LabelLogin }}</button>
|
||||
</form>
|
||||
{{ end }}
|
Reference in a new issue