Auth: registration/login/logout/whoami
diff --git a/core/auth/ui/Dockerfile b/core/auth/ui/Dockerfile
new file mode 100644
index 0000000..8a76b08
--- /dev/null
+++ b/core/auth/ui/Dockerfile
@@ -0,0 +1,4 @@
+FROM alpine:latest
+
+COPY server /usr/bin
+RUN chmod +x /usr/bin/server
diff --git a/core/auth/ui/Makefile b/core/auth/ui/Makefile
new file mode 100644
index 0000000..c444c97
--- /dev/null
+++ b/core/auth/ui/Makefile
@@ -0,0 +1,18 @@
+build:
+ go build -o server *.go
+
+clean:
+ rm -f server
+
+image: clean build
+ docker build --tag=giolekva/auth-ui .
+
+push: image
+ docker push giolekva/auth-ui:latest
+
+
+push_arm64: export GOOS=linux
+push_arm64: export GOARCH=arm64
+push_arm64: export CGO_ENABLED=0
+push_arm64: export GO111MODULE=on
+push_arm64: push
diff --git a/core/auth/ui/go.mod b/core/auth/ui/go.mod
new file mode 100644
index 0000000..49c33b1
--- /dev/null
+++ b/core/auth/ui/go.mod
@@ -0,0 +1,8 @@
+module github.com/giolekva/pcloud/core/auth/ui
+
+go 1.16
+
+require (
+ github.com/gorilla/mux v1.8.0
+ github.com/itaysk/regogo v0.0.0-20200423164851-e9433c1fe5a7
+)
diff --git a/core/auth/ui/go.sum b/core/auth/ui/go.sum
new file mode 100644
index 0000000..69c993e
--- /dev/null
+++ b/core/auth/ui/go.sum
@@ -0,0 +1,73 @@
+github.com/Jeffail/gabs/v2 v2.5.0 h1:ERXffrksCEPjKVDWbZDBcOwrpXctXfeFGXxOQh1umOE=
+github.com/Jeffail/gabs/v2 v2.5.0/go.mod h1:xCn81vdHKxFUuWWAaD5jCTQDNPBMh5pPs9IJ+NcziBI=
+github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
+github.com/OneOfOne/xxhash v1.2.7 h1:fzrmmkskv067ZQbd9wERNGuxckWw67dyzoMG62p7LMo=
+github.com/OneOfOne/xxhash v1.2.7/go.mod h1:eZbhyaAYD41SGSSsnmcpxVoRiQ/MPUTjUdIIOT9Um7Q=
+github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
+github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
+github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
+github.com/ghodss/yaml v0.0.0-20180820084758-c7ce16629ff4 h1:bRzFpEzvausOAt4va+I/22BZ1vXDtERngp0BNYDKej0=
+github.com/ghodss/yaml v0.0.0-20180820084758-c7ce16629ff4/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
+github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y=
+github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8=
+github.com/gogo/protobuf v1.3.0/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o=
+github.com/golang/protobuf v0.0.0-20181025225059-d3de96c4c28e/go.mod h1:Qd/q+1AKNOZr9uGQzbzCmRO6sUih6GTPZv6a1/R87v0=
+github.com/gorilla/mux v0.0.0-20181024020800-521ea7b17d02/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
+github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
+github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
+github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
+github.com/itaysk/regogo v0.0.0-20200423164851-e9433c1fe5a7 h1:I9EDcKhL1c3bU0GvVvxU87o+fAR9XAwCf+bDd+wxY5U=
+github.com/itaysk/regogo v0.0.0-20200423164851-e9433c1fe5a7/go.mod h1:QB9wWkHRsl6HnoMWvVMIzf07Wj7WtSW5IInTjfvnjek=
+github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00=
+github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
+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/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
+github.com/mattn/go-runewidth v0.0.0-20181025052659-b20a3daf6a39/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
+github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
+github.com/mna/pigeon v0.0.0-20180808201053-bb0192cfc2ae/go.mod h1:Iym28+kJVnC1hfQvv5MUtI6AiFFzvQjHcvI4RFTG/04=
+github.com/olekukonko/tablewriter v0.0.1/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo=
+github.com/open-policy-agent/opa v0.18.0 h1:EC81mO3/517Kq5brJHydqKE5MLzJ+4cdJvUQKxLzHy8=
+github.com/open-policy-agent/opa v0.18.0/go.mod h1:6pC1cMYDI92i9EY/GoA2m+HcZlcCrh3jbfny5F7JVTA=
+github.com/peterh/liner v0.0.0-20170211195444-bf27d3ba8e1d/go.mod h1:xIteQHvHuaLYG9IFj6mSxM0fCKrs34IrEQUhOYuGPHc=
+github.com/pkg/errors v0.0.0-20181023235946-059132a15dd0 h1:R+lX9nKwNd1n7UE5SQAyoorREvRn3aLF6ZndXBoIWqY=
+github.com/pkg/errors v0.0.0-20181023235946-059132a15dd0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
+github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/prometheus/client_golang v0.0.0-20181025174421-f30f42803563/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
+github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
+github.com/prometheus/common v0.0.0-20181020173914-7e9e6cabbd39/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
+github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
+github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a h1:9ZKAASQSHhDYGoxY8uLVpewe1GDZ2vu2Tr/vTdVAkFQ=
+github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
+github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q=
+github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
+github.com/spf13/cobra v0.0.0-20181021141114-fe5e611709b0/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ=
+github.com/spf13/pflag v0.0.0-20181024212040-082b515c9490/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
+github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
+github.com/tidwall/gjson v1.6.0 h1:9VEQWz6LLMUsUl6PueE49ir4Ka6CzLymOAZDxpFsTDc=
+github.com/tidwall/gjson v1.6.0/go.mod h1:P256ACg0Mn+j1RXIDXoss50DeIABTYK1PULOJHhxOls=
+github.com/tidwall/match v1.0.1 h1:PnKP62LPNxHKTwvHHZZzdOAOCtsJTjo6dZLCwpKm5xc=
+github.com/tidwall/match v1.0.1/go.mod h1:LujAq0jyVjBy028G1WhWfIzbpQfMO8bBZ6Tyb0+pL9E=
+github.com/tidwall/pretty v1.0.0 h1:HsD+QiTn7sK6flMKIvNmpqz1qrpP3Ps6jOKIKMooyg4=
+github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk=
+github.com/yashtewari/glob-intersection v0.0.0-20180916065949-5c77d914dd0b h1:vVRagRXf67ESqAb72hG2C/ZwI8NtJF2u2V76EsuOHGY=
+github.com/yashtewari/glob-intersection v0.0.0-20180916065949-5c77d914dd0b/go.mod h1:HptNXiXVDcJjXe9SqMd0v2FsL9f8dz4GnXgltU6q/co=
+golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
+golang.org/x/lint v0.0.0-20181023182221-1baf3a9d7d67/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
+golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20190920225731-5eefd052ad72/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+google.golang.org/genproto v0.0.0-20180831171423-11092d34479b/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
+gopkg.in/yaml.v2 v2.2.1 h1:mUhvW9EsL+naU5Q3cakzfE91YhliOondGd6ZrsDBHQE=
+gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
diff --git a/core/auth/ui/install.yaml b/core/auth/ui/install.yaml
new file mode 100644
index 0000000..ec82b9a
--- /dev/null
+++ b/core/auth/ui/install.yaml
@@ -0,0 +1,82 @@
+---
+apiVersion: v1
+kind: Namespace
+metadata:
+ name: core-auth
+---
+apiVersion: v1
+kind: Service
+metadata:
+ name: kratos-selfservice-ui
+ namespace: core-auth
+spec:
+ type: ClusterIP
+ selector:
+ app: kratos-selfservice-ui
+ ports:
+ - name: http
+ port: 80
+ targetPort: http
+ protocol: TCP
+---
+apiVersion: networking.k8s.io/v1
+kind: Ingress
+metadata:
+ name: ingress-kratos-selfservice-ui-public
+ namespace: core-auth
+ annotations:
+ cert-manager.io/cluster-issuer: "letsencrypt-prod"
+ acme.cert-manager.io/http01-edit-in-place: "true"
+spec:
+ ingressClassName: nginx
+ tls:
+ - hosts:
+ - accounts-ui.lekva.me
+ secretName: cert-accounts-ui.lekva.me
+ rules:
+ - host: accounts-ui.lekva.me
+ http:
+ paths:
+ - path: /
+ pathType: Prefix
+ backend:
+ service:
+ name: kratos-selfservice-ui
+ port:
+ name: http
+---
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+ name: kratos-selfservice-ui
+ namespace: core-auth
+spec:
+ selector:
+ matchLabels:
+ app: kratos-selfservice-ui
+ replicas: 1
+ template:
+ metadata:
+ labels:
+ app: kratos-selfservice-ui
+ spec:
+ containers:
+ - name: server
+ # image: giolekva/ory-kratos-selfservice-ui:latest
+ image: giolekva/auth-ui:latest
+ imagePullPolicy: Always
+ env:
+ - name: KRATOS_PUBLIC_URL
+ value: "https://accounts.lekva.me"
+ ports:
+ - name: http
+ containerPort: 8080
+ protocol: TCP
+ command: ["server", "--port=8080"]
+ # resources:
+ # requests:
+ # memory: "10Mi"
+ # cpu: "10m"
+ # limits:
+ # memory: "20Mi"
+ # cpu: "100m"
diff --git a/core/auth/ui/main.go b/core/auth/ui/main.go
new file mode 100644
index 0000000..0546802
--- /dev/null
+++ b/core/auth/ui/main.go
@@ -0,0 +1,344 @@
+package main
+
+import (
+ "bytes"
+ "embed"
+ "encoding/json"
+ "errors"
+ "flag"
+ "fmt"
+ "html/template"
+ "io"
+ "io/ioutil"
+ "log"
+ "net/http"
+ "net/http/cookiejar"
+ "net/url"
+
+ "github.com/gorilla/mux"
+ "github.com/itaysk/regogo"
+)
+
+var port = flag.Int("port", 8080, "Port to listen on")
+var kratos = flag.String("kratos", "https://accounts.lekva.me", "Kratos URL")
+
+var ErrNotLoggedIn = errors.New("Not logged in")
+
+//go:embed templates/*
+var tmpls embed.FS
+
+type Templates struct {
+ WhoAmI *template.Template
+ Registration *template.Template
+ Login *template.Template
+}
+
+func ParseTemplates(fs embed.FS) (*Templates, error) {
+ registration, err := template.ParseFS(fs, "templates/registration.html")
+ if err != nil {
+ return nil, err
+ }
+ login, err := template.ParseFS(fs, "templates/login.html")
+ if err != nil {
+ return nil, err
+ }
+ whoami, err := template.ParseFS(fs, "templates/whoami.html")
+ if err != nil {
+ return nil, err
+ }
+ return &Templates{whoami, registration, login}, nil
+}
+
+type Server struct {
+ kratos string
+ tmpls *Templates
+}
+
+func (s *Server) Start(port int) error {
+ r := mux.NewRouter()
+ http.Handle("/", r)
+ r.Path("/registration").Methods(http.MethodGet).HandlerFunc(s.registrationInitiate)
+ r.Path("/registration").Methods(http.MethodPost).HandlerFunc(s.registration)
+ r.Path("/login").Methods(http.MethodGet).HandlerFunc(s.loginInitiate)
+ r.Path("/login").Methods(http.MethodPost).HandlerFunc(s.login)
+ r.Path("/logout").Methods(http.MethodGet).HandlerFunc(s.logout)
+ r.Path("/").HandlerFunc(s.whoami)
+ fmt.Printf("Starting HTTP server on port: %d\n", port)
+ return http.ListenAndServe(fmt.Sprintf(":%d", port), nil)
+}
+
+func getCSRFToken(flowType, flow string, cookies []*http.Cookie) (string, error) {
+ jar, err := cookiejar.New(nil)
+ if err != nil {
+ return "", err
+ }
+ client := &http.Client{
+ Jar: jar,
+ }
+ b, err := url.Parse("https://accounts.lekva.me/self-service/" + flowType + "/browser")
+ if err != nil {
+ return "", err
+ }
+ client.Jar.SetCookies(b, cookies)
+ resp, err := client.Get(fmt.Sprintf("https://accounts.lekva.me/self-service/"+flowType+"/flows?id=%s", flow))
+ if err != nil {
+ return "", err
+ }
+ respBody, err := ioutil.ReadAll(resp.Body)
+ if err != nil {
+ return "", err
+ }
+ token, err := regogo.Get(string(respBody), "input.ui.nodes[0].attributes.value")
+ if err != nil {
+ return "", err
+ }
+ return token.String(), nil
+}
+
+func (s *Server) registrationInitiate(w http.ResponseWriter, r *http.Request) {
+ if err := r.ParseForm(); err != nil {
+ http.Error(w, err.Error(), http.StatusInternalServerError)
+ return
+ }
+ flow, ok := r.Form["flow"]
+ if !ok {
+ http.Redirect(w, r, s.kratos+"/self-service/registration/browser", http.StatusSeeOther)
+ return
+ }
+ csrfToken, err := getCSRFToken("registration", flow[0], r.Cookies())
+ if err != nil {
+ http.Error(w, err.Error(), http.StatusInternalServerError)
+ return
+ }
+ log.Println(csrfToken)
+ w.Header().Set("Content-Type", "text/html")
+ if err := s.tmpls.Registration.Execute(w, csrfToken); err != nil {
+ http.Error(w, err.Error(), http.StatusInternalServerError)
+ return
+ }
+}
+
+type regReq struct {
+ CSRFToken string `json:"csrf_token"`
+ Method string `json:"method"`
+ Password string `json:"password"`
+ Traits regReqTraits `json:"traits"`
+}
+
+type regReqTraits struct {
+ Username string `json:"username"`
+}
+
+func (s *Server) registration(w http.ResponseWriter, r *http.Request) {
+ if err := r.ParseForm(); err != nil {
+ http.Error(w, err.Error(), http.StatusInternalServerError)
+ return
+ }
+ flow, ok := r.Form["flow"]
+ if !ok {
+ http.Redirect(w, r, s.kratos+"/self-service/registration/browser", http.StatusSeeOther)
+ return
+ }
+ req := regReq{
+ CSRFToken: r.FormValue("csrf_token"),
+ Method: "password",
+ Password: r.FormValue("password"),
+ Traits: regReqTraits{
+ Username: r.FormValue("username"),
+ },
+ }
+ var reqBody bytes.Buffer
+ if err := json.NewEncoder(&reqBody).Encode(req); err != nil {
+ http.Error(w, err.Error(), http.StatusInternalServerError)
+ return
+ }
+ if resp, err := postToKratos("registration", flow[0], r.Cookies(), &reqBody); err != nil {
+ http.Error(w, err.Error(), http.StatusInternalServerError)
+ return
+ } else {
+ for _, c := range resp.Cookies() {
+ http.SetCookie(w, c)
+ }
+ http.Redirect(w, r, "/", http.StatusSeeOther)
+ }
+}
+
+// Login flow
+
+func (s *Server) loginInitiate(w http.ResponseWriter, r *http.Request) {
+ if err := r.ParseForm(); err != nil {
+ http.Error(w, err.Error(), http.StatusInternalServerError)
+ return
+ }
+ flow, ok := r.Form["flow"]
+ if !ok {
+ http.Redirect(w, r, s.kratos+"/self-service/login/browser", http.StatusSeeOther)
+ return
+ }
+ csrfToken, err := getCSRFToken("login", flow[0], r.Cookies())
+ if err != nil {
+ http.Error(w, err.Error(), http.StatusInternalServerError)
+ return
+ }
+ log.Println(csrfToken)
+ w.Header().Set("Content-Type", "text/html")
+ if err := s.tmpls.Login.Execute(w, csrfToken); err != nil {
+ http.Error(w, err.Error(), http.StatusInternalServerError)
+ return
+ }
+}
+
+type loginReq struct {
+ CSRFToken string `json:"csrf_token"`
+ Method string `json:"method"`
+ Password string `json:"password"`
+ Username string `json:"password_identifier"`
+}
+
+func postToKratos(flowType, flow string, cookies []*http.Cookie, req io.Reader) (*http.Response, error) {
+ jar, err := cookiejar.New(nil)
+ if err != nil {
+ return nil, err
+ }
+ client := &http.Client{
+ Jar: jar,
+ }
+ b, err := url.Parse("https://accounts.lekva.me/self-service/" + flowType + "/browser")
+ if err != nil {
+ return nil, err
+ }
+ client.Jar.SetCookies(b, cookies)
+ resp, err := client.Post(fmt.Sprintf("https://accounts.lekva.me/self-service/"+flowType+"?flow=%s", flow), "application/json", req)
+ if err != nil {
+ return nil, err
+ }
+ return resp, nil
+}
+
+type logoutResp struct {
+ LogoutURL string `json:"logout_url"`
+}
+
+func getLogoutURLFromKratos(cookies []*http.Cookie) (string, error) {
+ jar, err := cookiejar.New(nil)
+ if err != nil {
+ return "", err
+ }
+ client := &http.Client{
+ Jar: jar,
+ }
+ b, err := url.Parse("https://accounts.lekva.me/self-service/logout/browser")
+ if err != nil {
+ return "", err
+ }
+ client.Jar.SetCookies(b, cookies)
+ resp, err := client.Get("https://accounts.lekva.me/self-service/logout/browser")
+ if err != nil {
+ return "", err
+ }
+ var lr logoutResp
+ if err := json.NewDecoder(resp.Body).Decode(&lr); err != nil {
+ return "", err
+ }
+ return lr.LogoutURL, nil
+}
+
+func getWhoAmIFromKratos(cookies []*http.Cookie) (string, error) {
+ jar, err := cookiejar.New(nil)
+ if err != nil {
+ return "", err
+ }
+ client := &http.Client{
+ Jar: jar,
+ }
+ b, err := url.Parse("https://accounts.lekva.me/sessions/whoami")
+ if err != nil {
+ return "", err
+ }
+ client.Jar.SetCookies(b, cookies)
+ resp, err := client.Get("https://accounts.lekva.me/sessions/whoami")
+ if err != nil {
+ return "", err
+ }
+ respBody, err := ioutil.ReadAll(resp.Body)
+ if err != nil {
+ return "", err
+ }
+ username, err := regogo.Get(string(respBody), "input.identity.traits.username")
+ if err != nil {
+ return "", err
+ }
+ if username.String() == "" {
+ return "", ErrNotLoggedIn
+ }
+ return username.String(), nil
+
+}
+
+func (s *Server) login(w http.ResponseWriter, r *http.Request) {
+ if err := r.ParseForm(); err != nil {
+ http.Error(w, err.Error(), http.StatusInternalServerError)
+ return
+ }
+ flow, ok := r.Form["flow"]
+ if !ok {
+ http.Redirect(w, r, s.kratos+"/self-service/login/browser", http.StatusSeeOther)
+ return
+ }
+ req := loginReq{
+ CSRFToken: r.FormValue("csrf_token"),
+ Method: "password",
+ Password: r.FormValue("password"),
+ Username: r.FormValue("username"),
+ }
+ var reqBody bytes.Buffer
+ if err := json.NewEncoder(&reqBody).Encode(req); err != nil {
+ http.Error(w, err.Error(), http.StatusInternalServerError)
+ return
+ }
+ if resp, err := postToKratos("login", flow[0], r.Cookies(), &reqBody); err != nil {
+ http.Error(w, err.Error(), http.StatusInternalServerError)
+ return
+ } else {
+ for _, c := range resp.Cookies() {
+ http.SetCookie(w, c)
+ }
+ http.Redirect(w, r, "/", http.StatusSeeOther)
+ }
+}
+
+func (s *Server) logout(w http.ResponseWriter, r *http.Request) {
+ if logoutURL, err := getLogoutURLFromKratos(r.Cookies()); err != nil {
+ http.Error(w, err.Error(), http.StatusInternalServerError)
+ return
+ } else {
+ http.Redirect(w, r, logoutURL, http.StatusSeeOther)
+ }
+}
+
+func (s *Server) whoami(w http.ResponseWriter, r *http.Request) {
+ if username, err := getWhoAmIFromKratos(r.Cookies()); err != nil {
+ if errors.Is(err, ErrNotLoggedIn) {
+ http.Redirect(w, r, "/login", http.StatusSeeOther)
+ return
+ }
+ http.Error(w, err.Error(), http.StatusInternalServerError)
+ } else {
+ if err := s.tmpls.WhoAmI.Execute(w, username); err != nil {
+ http.Error(w, err.Error(), http.StatusInternalServerError)
+ }
+ }
+}
+
+func main() {
+ flag.Parse()
+ t, err := ParseTemplates(tmpls)
+ if err != nil {
+ log.Fatal(err)
+ }
+ s := &Server{
+ kratos: *kratos,
+ tmpls: t,
+ }
+ log.Fatal(s.Start(*port))
+}
diff --git a/core/auth/ui/server b/core/auth/ui/server
new file mode 100755
index 0000000..042afdd
--- /dev/null
+++ b/core/auth/ui/server
Binary files differ
diff --git a/core/auth/ui/templates/login.html b/core/auth/ui/templates/login.html
new file mode 100644
index 0000000..04d0435
--- /dev/null
+++ b/core/auth/ui/templates/login.html
@@ -0,0 +1,22 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+ <meta charset="utf-8" />
+ <title>Login</title>
+</head>
+<body>
+ <a href="/">whoami</a>
+ <a href="/login">login</a>
+ <a href="/logout">logout</a>
+ <a href="/registration">registration</a><br/>
+ <form action="" method="POST">
+ <label for="username">Username:</label><br />
+ <input type="text" name="username" /><br />
+ <label for="password">Password:</label><br />
+ <input type="password" name="password" /><br />
+ <input type="hidden" name="csrf_token" value="{{.}}" /><br />
+ <input type="submit" value="Login" />
+ </form>
+</body>
+</html>
+
diff --git a/core/auth/ui/templates/registration.html b/core/auth/ui/templates/registration.html
new file mode 100644
index 0000000..6a10af8
--- /dev/null
+++ b/core/auth/ui/templates/registration.html
@@ -0,0 +1,22 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+ <meta charset="utf-8" />
+ <title>Create New Account</title>
+</head>
+<body>
+ <a href="/">whoami</a>
+ <a href="/login">login</a>
+ <a href="/logout">logout</a>
+ <a href="/registration">registration</a><br/>
+ <form action="" method="POST">
+ <label for="username">Username:</label><br />
+ <input type="text" name="username" /><br />
+ <label for="password">Password:</label><br />
+ <input type="password" name="password" /><br />
+ <input type="hidden" name="csrf_token" value="{{.}}" /><br />
+ <input type="submit" value="Create New Account" />
+ </form>
+</body>
+</html>
+
diff --git a/core/auth/ui/templates/whoami.html b/core/auth/ui/templates/whoami.html
new file mode 100644
index 0000000..58a007b
--- /dev/null
+++ b/core/auth/ui/templates/whoami.html
@@ -0,0 +1,15 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+ <meta charset="utf-8" />
+ <title>Create New Account</title>
+</head>
+<body>
+ <a href="/">whoami</a>
+ <a href="/login">login</a>
+ <a href="/logout">logout</a>
+ <a href="/registration">registration</a><br/>
+ Hello {{.}}!
+</body>
+</html>
+