blob: 244a9b0cf12ad7aa5ab68a4671e4f6c214774812 [file] [log] [blame]
package main
import (
"bytes"
"crypto/tls"
"embed"
"encoding/json"
"errors"
"flag"
"fmt"
"html/template"
"io"
"io/ioutil"
"log"
"net/http"
"net/http/cookiejar"
"net/url"
"time"
"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 hydra = flag.String("hydra", "hydra.pcloud", "Hydra admin server address")
var emailDomain = flag.String("email-domain", "lekva.me", "Email domain")
var apiPort = flag.Int("api-port", 8081, "API Port to listen on")
var kratosAPI = flag.String("kratos-api", "", "Kratos API address")
var enableRegistration = flag.Bool("enable-registration", false, "If true account registration will be enabled")
var defaultReturnTo = flag.String("default-return-to", "", "Default redirect address after login")
var ErrNotLoggedIn = errors.New("Not logged in")
//go:embed templates/*
var tmpls embed.FS
//go:embed static
var static embed.FS
type Templates struct {
WhoAmI *template.Template
Register *template.Template
Login *template.Template
ChangePassword *template.Template
ChangePasswordSuccess *template.Template
Error *template.Template
}
func ParseTemplates(fs embed.FS) (*Templates, error) {
base, err := template.ParseFS(fs, "templates/base.html")
if err != nil {
return nil, err
}
parse := func(path string) (*template.Template, error) {
if b, err := base.Clone(); err != nil {
return nil, err
} else {
return b.ParseFS(fs, path)
}
}
whoami, err := parse("templates/whoami.html")
if err != nil {
return nil, err
}
register, err := parse("templates/register.html")
if err != nil {
return nil, err
}
login, err := parse("templates/login.html")
if err != nil {
return nil, err
}
changePassword, err := parse("templates/change-password.html")
if err != nil {
return nil, err
}
changePasswordSuccess, err := parse("templates/change-password-success.html")
if err != nil {
return nil, err
}
errorPage, err := parse("templates/error.html")
if err != nil {
return nil, err
}
return &Templates{whoami, register, login, changePassword, changePasswordSuccess, errorPage}, nil
}
const (
invalidLoginMessage = "Username or password is incorrect."
duplicateRegistrationMessage = "Username is not available."
expiredFlowMessage = "This form expired. Please try again."
registrationRejectedMessage = "Registration could not be completed. Please review your details and try again."
passwordChangeRejectedMessage = "Password could not be changed. Please choose a different password and try again."
authenticationUnavailableMessage = "Authentication service is temporarily unavailable. Please try again."
authNoticeCookieName = "auth_ui_notice"
authNoticeLoginInvalid = "login_invalid"
authNoticeFlowExpired = "flow_expired"
)
var errFlowExpired = errors.New("self-service flow expired")
type LoginPageData struct {
FormAction string
CSRFToken string
EnableRegistration bool
GeneralNotice string
}
type RegisterPageData struct {
FormAction string
CSRFToken string
Username string
UsernameErrors []ValidationError
PasswordErrors []ValidationError
GeneralError string
}
type ChangePasswordPageData struct {
Username string
CSRFToken string
FormAction string
PasswordErrors []ValidationError
GeneralError string
}
type AccountPageData struct {
Username string
}
type ErrorPageData struct {
Title string
Message string
Status int
RecoveryHref string
RecoveryText string
}
type oryFlowResponse struct {
ID string `json:"id"`
UI struct {
Nodes []struct {
Attributes struct {
Name string `json:"name"`
Value json.RawMessage `json:"value"`
} `json:"attributes"`
} `json:"nodes"`
Messages []struct {
ID int64 `json:"id"`
} `json:"messages"`
} `json:"ui"`
}
type oryErrorResponse struct {
Error struct {
ID string `json:"id"`
} `json:"error"`
}
type Server struct {
r *mux.Router
serv *http.Server
kratos string
hydra *HydraClient
tmpls *Templates
enableRegistration bool
api *APIServer
defaultReturnTo string
}
func NewServer(
port int,
kratos string,
hydra *HydraClient,
tmpls *Templates,
enableRegistration bool,
api *APIServer,
defaultReturnTo string,
) *Server {
r := mux.NewRouter()
serv := &http.Server{
Addr: fmt.Sprintf(":%d", port),
Handler: r,
}
return &Server{r, serv, kratos, hydra, tmpls, enableRegistration, api, defaultReturnTo}
}
func executeTemplate(tmpl *template.Template, data any) ([]byte, error) {
var page bytes.Buffer
if err := tmpl.Execute(&page, data); err != nil {
return nil, err
}
return page.Bytes(), nil
}
func writeTemplate(w http.ResponseWriter, status int, page []byte) {
w.Header().Set("Content-Type", "text/html; charset=utf-8")
w.WriteHeader(status)
_, _ = w.Write(page)
}
func renderTemplate(w http.ResponseWriter, tmpl *template.Template, status int, data any) {
page, err := executeTemplate(tmpl, data)
if err != nil {
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
writeTemplate(w, status, page)
}
func (s *Server) renderDependencyError(w http.ResponseWriter, context string) {
data := ErrorPageData{Message: authenticationUnavailableMessage, Status: http.StatusBadGateway}
switch context {
case "login":
data.Title = "Authentication unavailable"
data.RecoveryHref = "/login"
data.RecoveryText = "Try signing in again"
case "registration":
data.Title = "Registration unavailable"
if s.enableRegistration {
data.RecoveryHref = "/register"
data.RecoveryText = "Try registration again"
} else {
data.RecoveryHref = "/login"
data.RecoveryText = "Go to sign in"
}
default:
data.Title = "Account unavailable"
data.RecoveryHref = "/"
data.RecoveryText = "Back to account"
}
renderTemplate(w, s.tmpls.Error, data.Status, data)
}
func setAuthNotice(w http.ResponseWriter, code string) {
if code != authNoticeLoginInvalid && code != authNoticeFlowExpired {
return
}
http.SetCookie(w, &http.Cookie{
Name: authNoticeCookieName,
Value: code,
Path: "/",
MaxAge: 120,
Expires: time.Now().Add(2 * time.Minute),
HttpOnly: true,
SameSite: http.SameSiteLaxMode,
})
}
func clearAuthNotice(w http.ResponseWriter) {
http.SetCookie(w, &http.Cookie{
Name: authNoticeCookieName,
Value: "",
Path: "/",
MaxAge: -1,
HttpOnly: true,
SameSite: http.SameSiteLaxMode,
})
}
func pendingAuthNotice(r *http.Request, accepted ...string) (code string, clear bool) {
cookie, err := r.Cookie(authNoticeCookieName)
if err != nil {
return "", false
}
for _, acceptedCode := range accepted {
if cookie.Value == acceptedCode {
return cookie.Value, true
}
}
if cookie.Value != authNoticeLoginInvalid && cookie.Value != authNoticeFlowExpired {
return "", true
}
return "", false
}
func parseOryErrorID(body []byte) string {
var response oryErrorResponse
if json.Unmarshal(body, &response) != nil {
return ""
}
return response.Error.ID
}
func parseRetryFlow(body []byte) (flowID, csrfToken string, duplicate bool, err error) {
var response oryFlowResponse
if json.Unmarshal(body, &response) != nil || response.ID == "" {
return "", "", false, errors.New("invalid retry flow")
}
csrfCount := 0
for _, node := range response.UI.Nodes {
if node.Attributes.Name != "csrf_token" {
continue
}
csrfCount++
if json.Unmarshal(node.Attributes.Value, &csrfToken) != nil || csrfToken == "" {
return "", "", false, errors.New("invalid retry csrf token")
}
}
if csrfCount != 1 {
return "", "", false, errors.New("invalid retry csrf token count")
}
for _, message := range response.UI.Messages {
if message.ID == 4000007 {
duplicate = true
}
}
return response.ID, csrfToken, duplicate, nil
}
func readResponseBody(resp *http.Response) ([]byte, error) {
defer resp.Body.Close()
return ioutil.ReadAll(resp.Body)
}
func localFlowAction(path, flow string) string {
return path + "?flow=" + url.QueryEscape(flow)
}
func (s *Server) restartFlow(w http.ResponseWriter, r *http.Request, flowType string) {
setAuthNotice(w, authNoticeFlowExpired)
addr := s.kratos + "/self-service/" + flowType + "/browser"
if flowType == "login" {
returnTo := r.FormValue("return_to")
if returnTo == "" && s.defaultReturnTo != "" {
returnTo = s.defaultReturnTo
}
if returnTo != "" {
addr += fmt.Sprintf("?return_to=%s", returnTo)
}
}
http.Redirect(w, r, addr, http.StatusSeeOther)
}
func cacheControlWrapper(h http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// TODO(giolekva): enable caching
// w.Header().Set("Cache-Control", "max-age=2592000") // 30 days
h.ServeHTTP(w, r)
})
}
func (s *Server) Start() error {
var staticFS = http.FS(static)
fs := http.FileServer(staticFS)
s.r.PathPrefix("/static/").Handler(cacheControlWrapper(fs))
if s.enableRegistration {
s.r.Path("/register").Methods(http.MethodGet).HandlerFunc(s.registerInitiate)
s.r.Path("/register").Methods(http.MethodPost).HandlerFunc(s.register)
}
s.r.Path("/login").Methods(http.MethodGet).HandlerFunc(s.loginInitiate)
s.r.Path("/login").Methods(http.MethodPost).HandlerFunc(s.login)
s.r.Path("/consent").Methods(http.MethodGet).HandlerFunc(s.consent)
s.r.Path("/consent").Methods(http.MethodPost).HandlerFunc(s.processConsent)
s.r.Path("/logout").Methods(http.MethodGet).HandlerFunc(s.logout)
s.r.Path("/settings").Methods("POST").HandlerFunc(s.changePassword)
s.r.Path("/settings").Methods("GET").HandlerFunc(s.changePasswordForm)
s.r.Path("/").HandlerFunc(s.whoami)
return s.serv.ListenAndServe()
}
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,
Transport: &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
},
}
b, err := url.Parse(*kratos + "/self-service/" + flowType + "/browser")
if err != nil {
return "", err
}
client.Jar.SetCookies(b, cookies)
resp, err := client.Get(fmt.Sprintf(*kratos+"/self-service/"+flowType+"/flows?id=%s", flow))
if err != nil {
return "", err
}
respBody, err := readResponseBody(resp)
if err != nil {
return "", err
}
if resp.StatusCode == http.StatusGone && parseOryErrorID(respBody) == "self_service_flow_expired" {
return "", errFlowExpired
}
if resp.StatusCode < http.StatusOK || resp.StatusCode >= http.StatusMultipleChoices {
return "", errors.New("flow fetch failed")
}
_, token, _, err := parseRetryFlow(respBody)
if err != nil {
return "", err
}
return token, nil
}
func (s *Server) registerInitiate(w http.ResponseWriter, r *http.Request) {
if err := r.ParseForm(); err != nil {
http.Error(w, "Bad request.", http.StatusBadRequest)
return
}
flow := r.FormValue("flow")
if flow == "" {
http.Redirect(w, r, s.kratos+"/self-service/registration/browser", http.StatusSeeOther)
return
}
csrfToken, err := getCSRFToken("registration", flow, r.Cookies())
if errors.Is(err, errFlowExpired) {
s.restartFlow(w, r, "registration")
return
}
if err != nil {
s.renderDependencyError(w, "registration")
return
}
notice := ""
noticeCode, clearNotice := pendingAuthNotice(r, authNoticeFlowExpired)
if noticeCode == authNoticeFlowExpired {
notice = expiredFlowMessage
}
page, err := executeTemplate(s.tmpls.Register, RegisterPageData{
FormAction: localFlowAction(r.URL.Path, flow),
CSRFToken: csrfToken,
GeneralError: notice,
})
if err != nil {
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
if clearNotice {
clearAuthNotice(w)
}
writeTemplate(w, http.StatusOK, page)
}
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) register(w http.ResponseWriter, r *http.Request) {
if err := r.ParseForm(); err != nil {
http.Error(w, "Bad request.", http.StatusBadRequest)
return
}
flow := r.FormValue("flow")
if flow == "" {
http.Redirect(w, r, s.kratos+"/self-service/registration/browser", http.StatusSeeOther)
return
}
username := r.FormValue("username")
password := r.FormValue("password")
usernameErrors := validateUsername(username)
passwordErrors := validatePassword(password)
if len(usernameErrors)+len(passwordErrors) > 0 {
renderTemplate(w, s.tmpls.Register, http.StatusUnprocessableEntity, RegisterPageData{
FormAction: localFlowAction(r.URL.Path, flow),
CSRFToken: r.FormValue("csrf_token"),
Username: username,
UsernameErrors: usernameErrors,
PasswordErrors: passwordErrors,
})
return
}
req := regReq{
CSRFToken: r.FormValue("csrf_token"),
Method: "password",
Password: password,
Traits: regReqTraits{
Username: username,
},
}
var reqBody bytes.Buffer
if err := json.NewEncoder(&reqBody).Encode(req); err != nil {
s.renderDependencyError(w, "registration")
return
}
resp, err := postToKratos("registration", flow, r.Cookies(), &reqBody)
if err != nil {
s.renderDependencyError(w, "registration")
return
}
for _, cookie := range resp.Cookies() {
http.SetCookie(w, cookie)
}
if resp.StatusCode < http.StatusBadRequest {
resp.Body.Close()
http.Redirect(w, r, "/", http.StatusSeeOther)
return
}
body, err := readResponseBody(resp)
if err != nil {
s.renderDependencyError(w, "registration")
return
}
if (resp.StatusCode == http.StatusGone && parseOryErrorID(body) == "self_service_flow_expired") ||
(resp.StatusCode == http.StatusForbidden && parseOryErrorID(body) == "security_csrf_violation") {
s.restartFlow(w, r, "registration")
return
}
if resp.StatusCode != http.StatusBadRequest {
s.renderDependencyError(w, "registration")
return
}
retryFlow, retryCSRF, duplicate, err := parseRetryFlow(body)
if err != nil {
s.renderDependencyError(w, "registration")
return
}
status := http.StatusUnprocessableEntity
generalError := registrationRejectedMessage
if duplicate {
status = http.StatusConflict
generalError = duplicateRegistrationMessage
}
renderTemplate(w, s.tmpls.Register, status, RegisterPageData{
FormAction: localFlowAction(r.URL.Path, retryFlow),
CSRFToken: retryCSRF,
Username: username,
GeneralError: generalError,
})
}
// Login flow
func clearLoginChallengeCookie(w http.ResponseWriter) {
http.SetCookie(w, &http.Cookie{
Name: "login_challenge",
Value: "",
Path: "/",
MaxAge: -1,
HttpOnly: true,
SameSite: http.SameSiteLaxMode,
})
}
func (s *Server) loginInitiate(w http.ResponseWriter, r *http.Request) {
if err := r.ParseForm(); err != nil {
http.Error(w, "Bad request.", http.StatusBadRequest)
return
}
challenge, hasChallenge := r.Form["login_challenge"]
flow, hasFlow := r.Form["flow"]
if !hasChallenge && !hasFlow {
clearLoginChallengeCookie(w)
}
if hasChallenge {
_, username, err := getWhoAmIFromKratos(r.Cookies())
if err != nil && err != ErrNotLoggedIn {
s.renderDependencyError(w, "login")
return
}
if err == nil {
redirectTo, err := s.hydra.LoginAcceptChallenge(challenge[0], username)
if err != nil {
s.renderDependencyError(w, "login")
return
}
clearLoginChallengeCookie(w)
http.Redirect(w, r, redirectTo, http.StatusSeeOther)
return
}
// TODO(giolekva): encrypt
http.SetCookie(w, &http.Cookie{
Name: "login_challenge",
Value: challenge[0],
Path: "/",
HttpOnly: true,
SameSite: http.SameSiteLaxMode,
})
}
returnTo := r.FormValue("return_to")
if returnTo == "" && s.defaultReturnTo != "" {
returnTo = s.defaultReturnTo
}
if !hasFlow {
addr := s.kratos + "/self-service/login/browser"
if returnTo != "" {
addr += fmt.Sprintf("?return_to=%s", returnTo)
}
http.Redirect(w, r, addr, http.StatusSeeOther)
return
}
csrfToken, err := getCSRFToken("login", flow[0], r.Cookies())
if errors.Is(err, errFlowExpired) {
s.restartFlow(w, r, "login")
return
}
if err != nil {
s.renderDependencyError(w, "login")
return
}
notice := ""
noticeCode, clearNotice := pendingAuthNotice(r, authNoticeLoginInvalid, authNoticeFlowExpired)
switch noticeCode {
case authNoticeLoginInvalid:
notice = invalidLoginMessage
case authNoticeFlowExpired:
notice = expiredFlowMessage
}
page, err := executeTemplate(s.tmpls.Login, LoginPageData{
FormAction: localFlowAction(r.URL.Path, flow[0]),
CSRFToken: csrfToken,
EnableRegistration: s.enableRegistration,
GeneralNotice: notice,
})
if err != nil {
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
if clearNotice {
clearAuthNotice(w)
}
writeTemplate(w, http.StatusOK, page)
}
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,
Transport: &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
},
}
b, err := url.Parse(*kratos + "/self-service/" + flowType + "/browser")
if err != nil {
return nil, err
}
client.Jar.SetCookies(b, cookies)
resp, err := client.Post(fmt.Sprintf(*kratos+"/self-service/"+flowType+"?flow=%s", flow), "application/json", req)
if err != nil {
return nil, err
}
return resp, nil
}
func postFormToKratos(flowType, flow string, cookies []*http.Cookie, data url.Values) (*http.Response, error) {
jar, err := cookiejar.New(nil)
if err != nil {
return nil, err
}
client := &http.Client{
Jar: jar,
Transport: &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
},
CheckRedirect: func(req *http.Request, via []*http.Request) error {
return http.ErrUseLastResponse
},
}
b, err := url.Parse(*kratos + "/self-service/" + flowType + "/browser")
if err != nil {
return nil, err
}
client.Jar.SetCookies(b, cookies)
resp, err := client.PostForm(fmt.Sprintf(*kratos+"/self-service/"+flowType+"?flow=%s", flow), data)
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,
Transport: &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
},
}
b, err := url.Parse(*kratos + "/self-service/logout/browser")
if err != nil {
return "", err
}
client.Jar.SetCookies(b, cookies)
resp, err := client.Get(*kratos + "/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, string, error) {
jar, err := cookiejar.New(nil)
if err != nil {
return "", "", err
}
client := &http.Client{
Jar: jar,
Transport: &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
},
}
b, err := url.Parse(*kratos + "/sessions/whoami")
if err != nil {
return "", "", err
}
client.Jar.SetCookies(b, cookies)
resp, err := client.Get(*kratos + "/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
}
id, err := regogo.Get(string(respBody), "input.identity.id")
if err != nil {
return "", "", err
}
if id.String() == "" {
return "", "", ErrNotLoggedIn
}
return id.String(), username.String(), nil
}
func isRejectedLoginRedirect(resp *http.Response) bool {
if resp.StatusCode != http.StatusSeeOther {
return false
}
location, err := url.Parse(resp.Header.Get("Location"))
return err == nil && location.Path == "/login" && location.Query().Get("flow") != ""
}
func (s *Server) login(w http.ResponseWriter, r *http.Request) {
if err := r.ParseForm(); err != nil {
http.Error(w, "Bad request.", http.StatusBadRequest)
return
}
flow := r.FormValue("flow")
if flow == "" {
http.Redirect(w, r, s.kratos+"/self-service/login/browser", http.StatusSeeOther)
return
}
req := url.Values{
"csrf_token": {r.FormValue("csrf_token")},
"method": {"password"},
"password": {r.FormValue("password")},
"identifier": {r.FormValue("username")},
}
resp, err := postFormToKratos("login", flow, r.Cookies(), req)
if err != nil {
s.renderDependencyError(w, "login")
return
}
defer resp.Body.Close()
var responseBody bytes.Buffer
_, _ = io.Copy(&responseBody, resp.Body)
fmt.Println(responseBody.String())
for _, cookie := range resp.Cookies() {
http.SetCookie(w, cookie)
}
if (resp.StatusCode == http.StatusGone && parseOryErrorID(responseBody.Bytes()) == "self_service_flow_expired") ||
(resp.StatusCode == http.StatusForbidden && parseOryErrorID(responseBody.Bytes()) == "security_csrf_violation") {
s.restartFlow(w, r, "login")
return
}
if isRejectedLoginRedirect(resp) {
setAuthNotice(w, authNoticeLoginInvalid)
http.Redirect(w, r, resp.Header.Get("Location"), http.StatusSeeOther)
return
}
if resp.StatusCode != http.StatusSeeOther {
s.renderDependencyError(w, "login")
return
}
if challenge, _ := r.Cookie("login_challenge"); challenge != nil {
_, username, err := getWhoAmIFromKratos(resp.Cookies())
if err != nil {
s.renderDependencyError(w, "login")
return
}
redirectTo, err := s.hydra.LoginAcceptChallenge(challenge.Value, username)
if err != nil {
s.renderDependencyError(w, "login")
return
}
clearLoginChallengeCookie(w)
http.Redirect(w, r, redirectTo, http.StatusSeeOther)
return
}
if resp.StatusCode == http.StatusSeeOther {
http.Redirect(w, r, resp.Header.Get("Location"), http.StatusSeeOther)
} else {
http.Redirect(w, r, "/", http.StatusSeeOther)
}
}
func (s *Server) logout(w http.ResponseWriter, r *http.Request) {
if logoutURL, err := getLogoutURLFromKratos(r.Cookies()); err != nil {
s.renderDependencyError(w, "account")
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
}
s.renderDependencyError(w, "account")
} else {
renderTemplate(w, s.tmpls.WhoAmI, http.StatusOK, AccountPageData{Username: username})
}
}
// TODO(giolekva): verify if logged in
func (s *Server) consent(w http.ResponseWriter, r *http.Request) {
if err := r.ParseForm(); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
challenge, ok := r.Form["consent_challenge"]
if !ok {
http.Error(w, "Consent challenge not provided", http.StatusBadRequest)
return
}
consent, err := s.hydra.GetConsentChallenge(challenge[0])
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
_, username, err := getWhoAmIFromKratos(r.Cookies())
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
acceptedScopes := consent.RequestedScopes
idToken := map[string]string{
"username": username,
"email": username + "@" + *emailDomain,
}
// TODO(gio): is auto consent safe? should such behaviour be configurable?
if redirectTo, err := s.hydra.ConsentAccept(r.FormValue("consent_challenge"), acceptedScopes, idToken); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
} else {
http.Redirect(w, r, redirectTo, http.StatusSeeOther)
}
}
func (s *Server) processConsent(w http.ResponseWriter, r *http.Request) {
if err := r.ParseForm(); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
_, username, err := getWhoAmIFromKratos(r.Cookies())
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
if _, accepted := r.Form["allow"]; accepted {
acceptedScopes, _ := r.Form["scope"]
idToken := map[string]string{
"username": username,
"email": username + "@" + *emailDomain,
}
if redirectTo, err := s.hydra.ConsentAccept(r.FormValue("consent_challenge"), acceptedScopes, idToken); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
} else {
http.Redirect(w, r, redirectTo, http.StatusSeeOther)
}
return
} else {
// TODO(giolekva): implement rejection logic
}
}
func (s *Server) changePasswordForm(w http.ResponseWriter, r *http.Request) {
flow := r.FormValue("flow")
if flow == "" {
http.Redirect(w, r, s.kratos+"/self-service/settings/browser", http.StatusSeeOther)
return
}
_, username, err := getWhoAmIFromKratos(r.Cookies())
if err != nil {
s.renderDependencyError(w, "account")
return
}
csrfToken, err := getCSRFToken("settings", flow, r.Cookies())
if errors.Is(err, errFlowExpired) {
s.restartFlow(w, r, "settings")
return
}
if err != nil {
s.renderDependencyError(w, "account")
return
}
notice := ""
noticeCode, clearNotice := pendingAuthNotice(r, authNoticeFlowExpired)
if noticeCode == authNoticeFlowExpired {
notice = expiredFlowMessage
}
page, err := executeTemplate(s.tmpls.ChangePassword, ChangePasswordPageData{
Username: username,
CSRFToken: csrfToken,
FormAction: localFlowAction(r.URL.Path, flow),
GeneralError: notice,
})
if err != nil {
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
if clearNotice {
clearAuthNotice(w)
}
writeTemplate(w, http.StatusOK, page)
}
func (s *Server) changePassword(w http.ResponseWriter, r *http.Request) {
if err := r.ParseForm(); err != nil {
http.Error(w, "Bad request.", http.StatusBadRequest)
return
}
flow := r.FormValue("flow")
if flow == "" {
http.Redirect(w, r, s.kratos+"/self-service/settings/browser", http.StatusSeeOther)
return
}
password := r.FormValue("password")
_, username, err := getWhoAmIFromKratos(r.Cookies())
if err != nil {
s.renderDependencyError(w, "account")
return
}
if passwordErrors := validatePassword(password); len(passwordErrors) > 0 {
renderTemplate(w, s.tmpls.ChangePassword, http.StatusUnprocessableEntity, ChangePasswordPageData{
Username: username,
CSRFToken: r.FormValue("csrf_token"),
FormAction: localFlowAction(r.URL.Path, flow),
PasswordErrors: passwordErrors,
})
return
}
resp, err := postFormToKratos("settings", flow, r.Cookies(), url.Values{
"csrf_token": {r.FormValue("csrf_token")},
"method": {"password"},
"password": {password},
})
if err != nil {
s.renderDependencyError(w, "account")
return
}
if resp.StatusCode < http.StatusBadRequest {
resp.Body.Close()
renderTemplate(w, s.tmpls.ChangePasswordSuccess, http.StatusOK, nil)
return
}
body, err := readResponseBody(resp)
if err != nil {
s.renderDependencyError(w, "account")
return
}
if (resp.StatusCode == http.StatusGone && parseOryErrorID(body) == "self_service_flow_expired") ||
(resp.StatusCode == http.StatusForbidden && parseOryErrorID(body) == "security_csrf_violation") {
s.restartFlow(w, r, "settings")
return
}
if resp.StatusCode != http.StatusBadRequest {
s.renderDependencyError(w, "account")
return
}
retryFlow, retryCSRF, _, err := parseRetryFlow(body)
if err != nil {
s.renderDependencyError(w, "account")
return
}
renderTemplate(w, s.tmpls.ChangePassword, http.StatusUnprocessableEntity, ChangePasswordPageData{
Username: username,
CSRFToken: retryCSRF,
FormAction: localFlowAction(r.URL.Path, retryFlow),
GeneralError: passwordChangeRejectedMessage,
})
}
func main() {
flag.Parse()
t, err := ParseTemplates(tmpls)
if err != nil {
log.Fatal(err)
}
api := NewAPIServer(*apiPort, *kratosAPI)
go func() {
log.Fatal(api.Start())
}()
func() {
s := NewServer(
*port,
*kratos,
NewHydraClient(*hydra),
t,
*enableRegistration,
api,
*defaultReturnTo,
)
log.Fatal(s.Start())
}()
}