diff options
Diffstat (limited to 'pkg/app/handler/authentication')
-rw-r--r-- | pkg/app/handler/authentication/accessDenied.go | 10 | ||||
-rw-r--r-- | pkg/app/handler/authentication/auth_session/authsession.go | 177 | ||||
-rw-r--r-- | pkg/app/handler/authentication/login.go | 100 | ||||
-rw-r--r-- | pkg/app/handler/authentication/logout.go | 29 | ||||
-rw-r--r-- | pkg/app/handler/authentication/templates/admin.go | 36 | ||||
-rw-r--r-- | pkg/app/handler/authentication/templates/login.go | 32 | ||||
-rw-r--r-- | pkg/app/handler/authentication/templates/totp.go | 23 | ||||
-rw-r--r-- | pkg/app/handler/authentication/templates/webauthn.go | 23 | ||||
-rw-r--r-- | pkg/app/handler/authentication/totp/totp.go | 60 | ||||
-rw-r--r-- | pkg/app/handler/authentication/utils/utils.go | 81 | ||||
-rw-r--r-- | pkg/app/handler/authentication/webauthn/login.go | 118 | ||||
-rw-r--r-- | pkg/app/handler/authentication/webauthn/register.go | 111 |
12 files changed, 800 insertions, 0 deletions
diff --git a/pkg/app/handler/authentication/accessDenied.go b/pkg/app/handler/authentication/accessDenied.go new file mode 100644 index 0000000..de06ab2 --- /dev/null +++ b/pkg/app/handler/authentication/accessDenied.go @@ -0,0 +1,10 @@ +package authentication + +import ( + "glsamaker/pkg/app/handler/authentication/templates" + "net/http" +) + +func AccessDenied(w http.ResponseWriter, r *http.Request) { + templates.RenderAccessDeniedTemplate(w, r) +} diff --git a/pkg/app/handler/authentication/auth_session/authsession.go b/pkg/app/handler/authentication/auth_session/authsession.go new file mode 100644 index 0000000..c86ca99 --- /dev/null +++ b/pkg/app/handler/authentication/auth_session/authsession.go @@ -0,0 +1,177 @@ +package auth_session + +import ( + "glsamaker/pkg/database/connection" + "glsamaker/pkg/logger" + "glsamaker/pkg/models" + "glsamaker/pkg/models/users" + "github.com/google/uuid" + "net/http" + "strings" + "time" +) + +func Create(w http.ResponseWriter, r *http.Request, user *users.User, bindSessionToIP bool, secondFactorMissing bool) { + sessionID := createSessionID() + sessionIP := "*" + expires := time.Now().AddDate(0, 1, 0) + + if bindSessionToIP { + sessionIP = getIP(r) + } + + if secondFactorMissing { + expires = time.Now().Add(10 * time.Minute) + } + + session := &models.Session{ + Id: sessionID, + UserId: user.Id, + IP: sessionIP, + SecondFactorMissing: secondFactorMissing, + Expires: expires, + } + + _, err := connection.DB.Model(session).OnConflict("(id) DO UPDATE").Insert() + if err != nil { + logger.Error.Println("Err during creating session") + logger.Error.Println(err) + } + + createSessionCookie(w, sessionID) +} + +func createSessionID() string { + id, _ := uuid.NewUUID() + return id.String() +} + +func createSessionCookie(w http.ResponseWriter, sessionID string) { + + expires := time.Now().AddDate(0, 1, 0) + + ck := http.Cookie{ + Name: "session", + Domain: "localhost", + Path: "/", + Expires: expires, + } + + ck.Value = sessionID + + http.SetCookie(w, &ck) + +} + +func GetUserId(sessionId, userIP string) int64 { + session := &models.Session{Id: sessionId} + err := connection.DB.Model(session).Relation("User").WherePK().Select() + + if err != nil || session.User.Disabled { + return -1 + } + + if session != nil && + session.Expires.After(time.Now()) && + isValidIP(session.IP, userIP) { + return session.UserId + } else { + return -1 + } +} + +func Only2FAMissing(sessionId, userIP string) bool { + session := &models.Session{Id: sessionId} + err := connection.DB.Model(session).Relation("User").WherePK().Select() + + if err != nil { + return false + } + + invalidateExpiredSession(session) + + return session != nil && + session.Expires.After(time.Now()) && + !session.User.Disabled && + session.SecondFactorMissing && + isValidIP(session.IP, userIP) +} + +func IsLoggedIn(sessionId, userIP string) bool { + + session := &models.Session{Id: sessionId} + err := connection.DB.Model(session).Relation("User").WherePK().Select() + + if err != nil { + return false + } + + invalidateExpiredSession(session) + + return session != nil && + !session.SecondFactorMissing && + !session.User.Disabled && + session.Expires.After(time.Now()) && + isValidIP(session.IP, userIP) +} + +func IsLoggedInAndNeedsNewPassword(sessionId, userIP string) bool { + + session := &models.Session{Id: sessionId} + err := connection.DB.Model(session).Relation("User").WherePK().Select() + + if err != nil { + return false + } + + invalidateExpiredSession(session) + + return session != nil && + !session.SecondFactorMissing && + !session.User.Disabled && + session.User.ForcePasswordRotation && + session.Expires.After(time.Now()) && + isValidIP(session.IP, userIP) +} + +func IsLoggedInAndNeeds2FA(sessionId, userIP string) bool { + + session := &models.Session{Id: sessionId} + err := connection.DB.Model(session).Relation("User").WherePK().Select() + + if err != nil { + return false + } + + invalidateExpiredSession(session) + + return session != nil && + !session.SecondFactorMissing && + !session.User.Disabled && + session.User.Force2FA && + !session.User.IsUsing2FA() && + session.Expires.After(time.Now()) && + isValidIP(session.IP, userIP) +} + +func invalidateExpiredSession(session *models.Session) { + if session.Expires.Before(time.Now()) { + _, err := connection.DB.Model(session).WherePK().Delete() + if err != nil { + logger.Error.Println("Error deleting expired session.") + logger.Error.Println(err) + } + } +} + +func isValidIP(sessionIP, userIP string) bool { + return sessionIP == "*" || userIP == sessionIP +} + +func getIP(r *http.Request) string { + forwarded := r.Header.Get("X-FORWARDED-FOR") + if forwarded != "" { + return strings.Split(forwarded, ":")[0] + } + return strings.Split(r.RemoteAddr, ":")[0] +} diff --git a/pkg/app/handler/authentication/login.go b/pkg/app/handler/authentication/login.go new file mode 100644 index 0000000..7cd5c87 --- /dev/null +++ b/pkg/app/handler/authentication/login.go @@ -0,0 +1,100 @@ +package authentication + +import ( + "glsamaker/pkg/app/handler/authentication/auth_session" + "glsamaker/pkg/app/handler/authentication/templates" + "glsamaker/pkg/app/handler/authentication/utils" + "glsamaker/pkg/database/connection" + "glsamaker/pkg/models/users" + "golang.org/x/crypto/argon2" + "net/http" +) + +func Login(w http.ResponseWriter, r *http.Request) { + + // in case '/login' is request but the user is + // already authenticated we will redirect to '/' + if utils.IsAuthenticated(w, r) { + http.Redirect(w, r, "/", 301) + } + + username, pass, cameFrom, bindLoginToIP, _ := getParams(r) + + if IsValidPassword(username, pass) { + user, _ := getLoginUser(username) + auth_session.Create(w, r, user, bindLoginToIP, user.IsUsing2FA()) + if user.IsUsing2FA() { + http.Redirect(w, r, "/login/2fa", 301) + } else { + http.Redirect(w, r, cameFrom, 301) + } + } else { + templates.RenderLoginTemplate(w, r) + } + +} + +func SecondFactorLogin(w http.ResponseWriter, r *http.Request) { + user := utils.GetAuthenticatedUser(r) + + if user == nil || !user.IsUsing2FA() { + // this should not occur + http.NotFound(w, r) + return + } + + if user.IsUsingTOTP { + templates.RenderTOTPTemplate(w, r) + } else if user.IsUsingWebAuthn { + templates.RenderWebAuthnTemplate(w, r) + } else { + // this should not occur + http.NotFound(w, r) + } +} + +// utility functions + +func getLoginUser(username string) (*users.User, bool) { + var potenialUsers []*users.User + err := connection.DB.Model(&potenialUsers).Where("nick = ?", username).Select() + isValidUser := err == nil + + if len(potenialUsers) < 1 { + return &users.User{}, false + } + + return potenialUsers[0], isValidUser +} + +func getParams(r *http.Request) (string, string, string, bool, error) { + err := r.ParseForm() + if err != nil { + return "", "", "", false, err + } + username := r.Form.Get("username") + password := r.Form.Get("password") + cameFrom := r.Form.Get("cameFrom") + restrictLogin := r.Form.Get("restrictlogin") + return username, password, cameFrom, restrictLogin == "on", err +} + +func IsValidPassword(username string, password string) bool { + user, isValidUser := getLoginUser(username) + if !isValidUser { + return false + } + + hashedPassword := argon2.IDKey( + []byte(password), + user.Password.Salt, + user.Password.Time, + user.Password.Memory, + user.Password.Threads, + user.Password.KeyLen) + + if user != nil && !user.Disabled && string(user.Password.Hash) == string(hashedPassword) { + return true + } + return false +} diff --git a/pkg/app/handler/authentication/logout.go b/pkg/app/handler/authentication/logout.go new file mode 100644 index 0000000..87d17f4 --- /dev/null +++ b/pkg/app/handler/authentication/logout.go @@ -0,0 +1,29 @@ +package authentication + +import ( + "glsamaker/pkg/database/connection" + "glsamaker/pkg/logger" + "glsamaker/pkg/models" + "net/http" +) + +func Logout(w http.ResponseWriter, r *http.Request) { + + sessionID, err := r.Cookie("session") + + if err != nil || sessionID == nil { + // TODO Error + } + + session := &models.Session{Id: sessionID.Value} + _, err = connection.DB.Model(session).WherePK().Delete() + + if err != nil { + logger.Info.Println("Error deleting session") + logger.Error.Println("Error deleting session") + logger.Error.Println(err) + } + + http.Redirect(w, r, "/", 301) + +} diff --git a/pkg/app/handler/authentication/templates/admin.go b/pkg/app/handler/authentication/templates/admin.go new file mode 100644 index 0000000..12d039e --- /dev/null +++ b/pkg/app/handler/authentication/templates/admin.go @@ -0,0 +1,36 @@ +package templates + +import ( + "glsamaker/pkg/app/handler/authentication/utils" + "glsamaker/pkg/models" + "glsamaker/pkg/models/users" + "html/template" + "net/http" +) + +// renderIndexTemplate renders all templates used for the login page +func RenderAccessDeniedTemplate(w http.ResponseWriter, r *http.Request) { + + user := utils.GetAuthenticatedUser(r) + + templates := template.Must( + template.Must( + template.New("Show"). + ParseGlob("web/templates/layout/*.tmpl")). + ParseGlob("web/templates/authentication/accessDenied.tmpl")) + + templates.ExecuteTemplate(w, "accessDenied.tmpl", createAccessDeniedData(user)) +} + +// createPageData creates the data used in the template of the landing page +func createAccessDeniedData(user *users.User) interface{} { + return struct { + Page string + Application *models.GlobalSettings + User *users.User + }{ + Page: "", + Application: models.GetDefaultGlobalSettings(), + User: user, + } +} diff --git a/pkg/app/handler/authentication/templates/login.go b/pkg/app/handler/authentication/templates/login.go new file mode 100644 index 0000000..2e3b241 --- /dev/null +++ b/pkg/app/handler/authentication/templates/login.go @@ -0,0 +1,32 @@ +package templates + +import ( + "html/template" + "net/http" +) + +// renderIndexTemplate renders all templates used for the login page +func RenderLoginTemplate(w http.ResponseWriter, r *http.Request) { + + data := struct { + CameFrom string + }{ + CameFrom: getPath(r), + } + + templates := template.Must( + template.Must( + template.New("Show"). + ParseGlob("web/templates/layout/*.tmpl")). + ParseGlob("web/templates/authentication/login.tmpl")) + + templates.ExecuteTemplate(w, "login.tmpl", data) +} + +func getPath(r *http.Request) string { + if r.URL.RawQuery == "" { + return r.URL.Path + } else { + return r.URL.Path + "?" + r.URL.RawQuery + } +} diff --git a/pkg/app/handler/authentication/templates/totp.go b/pkg/app/handler/authentication/templates/totp.go new file mode 100644 index 0000000..acb34e5 --- /dev/null +++ b/pkg/app/handler/authentication/templates/totp.go @@ -0,0 +1,23 @@ +package templates + +import ( + "html/template" + "net/http" +) + +func RenderTOTPTemplate(w http.ResponseWriter, r *http.Request) { + + data := struct { + CameFrom string + }{ + CameFrom: getPath(r), + } + + templates := template.Must( + template.Must( + template.New("Show"). + ParseGlob("web/templates/layout/*.tmpl")). + ParseGlob("web/templates/authentication/totp.tmpl")) + + templates.ExecuteTemplate(w, "totp.tmpl", data) +} diff --git a/pkg/app/handler/authentication/templates/webauthn.go b/pkg/app/handler/authentication/templates/webauthn.go new file mode 100644 index 0000000..148f475 --- /dev/null +++ b/pkg/app/handler/authentication/templates/webauthn.go @@ -0,0 +1,23 @@ +package templates + +import ( + "html/template" + "net/http" +) + +func RenderWebAuthnTemplate(w http.ResponseWriter, r *http.Request) { + + data := struct { + CameFrom string + }{ + CameFrom: getPath(r), + } + + templates := template.Must( + template.Must( + template.New("Show"). + ParseGlob("web/templates/layout/*.tmpl")). + ParseGlob("web/templates/authentication/webauthn.tmpl")) + + templates.ExecuteTemplate(w, "webauthn.tmpl", data) +} diff --git a/pkg/app/handler/authentication/totp/totp.go b/pkg/app/handler/authentication/totp/totp.go new file mode 100644 index 0000000..00e6b83 --- /dev/null +++ b/pkg/app/handler/authentication/totp/totp.go @@ -0,0 +1,60 @@ +package totp + +import ( + "glsamaker/pkg/app/handler/authentication/auth_session" + "glsamaker/pkg/app/handler/authentication/utils" + "glsamaker/pkg/models/users" + "bytes" + "encoding/base64" + "github.com/pquerna/otp/totp" + "image/png" + "net/http" + "time" +) + +func Login(w http.ResponseWriter, r *http.Request) { + + user := utils.GetAuthenticatedUser(r) + token, err := getParam(r) + + if user == nil || err != nil || !IsValidTOTPToken(user, token) { + http.Redirect(w, r, "/login/2fa", 301) + } else { + auth_session.Create(w, r, user, true, false) + http.Redirect(w, r, "/", 301) + } + +} + +func IsValidTOTPToken(user *users.User, token string) bool { + return totp.Validate(token, user.TOTPSecret) +} + +func GetToken(user *users.User) string { + token, _ := totp.GenerateCode(user.TOTPSecret, time.Now()) + return token +} + +func Generate(email string) (string, string) { + + key, _ := totp.Generate(totp.GenerateOpts{ + Issuer: "glsamakertest.gentoo.org", + AccountName: email, + }) + + var buf bytes.Buffer + img, _ := key.Image(250, 250) + + png.Encode(&buf, img) + + return key.Secret(), base64.StdEncoding.EncodeToString(buf.Bytes()) +} + +func getParam(r *http.Request) (string, error) { + err := r.ParseForm() + if err != nil { + return "", err + } + token := r.Form.Get("token") + return token, err +} diff --git a/pkg/app/handler/authentication/utils/utils.go b/pkg/app/handler/authentication/utils/utils.go new file mode 100644 index 0000000..d06a2d7 --- /dev/null +++ b/pkg/app/handler/authentication/utils/utils.go @@ -0,0 +1,81 @@ +package utils + +import ( + "glsamaker/pkg/app/handler/authentication/auth_session" + "glsamaker/pkg/database/connection" + "glsamaker/pkg/models/users" + "net/http" + "strings" +) + +// utility methods to check whether a user is authenticated + +func Only2FAMissing(w http.ResponseWriter, r *http.Request) bool { + sessionID, err := r.Cookie("session") + userIP := getIP(r) + + return err == nil && sessionID != nil && auth_session.Only2FAMissing(sessionID.Value, userIP) +} + +func IsAuthenticated(w http.ResponseWriter, r *http.Request) bool { + sessionID, err := r.Cookie("session") + userIP := getIP(r) + + return err == nil && sessionID != nil && auth_session.IsLoggedIn(sessionID.Value, userIP) +} + +func IsAuthenticatedAndNeedsNewPassword(w http.ResponseWriter, r *http.Request) bool { + sessionID, err := r.Cookie("session") + userIP := getIP(r) + + return err == nil && sessionID != nil && auth_session.IsLoggedInAndNeedsNewPassword(sessionID.Value, userIP) +} + +func IsAuthenticatedAndNeeds2FA(w http.ResponseWriter, r *http.Request) bool { + sessionID, err := r.Cookie("session") + userIP := getIP(r) + + return err == nil && sessionID != nil && auth_session.IsLoggedInAndNeeds2FA(sessionID.Value, userIP) +} + +func IsAuthenticatedAsAdmin(w http.ResponseWriter, r *http.Request) bool { + sessionID, err := r.Cookie("session") + userIP := getIP(r) + + if err != nil || sessionID == nil || !auth_session.IsLoggedIn(sessionID.Value, userIP) { + return false + } + + user := GetAuthenticatedUser(r) + + return user != nil && user.Permissions.Admin.View + +} + +func GetAuthenticatedUser(r *http.Request) *users.User { + sessionID, err := r.Cookie("session") + userIP := getIP(r) + + if err != nil || sessionID == nil || !(auth_session.IsLoggedIn(sessionID.Value, userIP) || auth_session.Only2FAMissing(sessionID.Value, userIP)) { + return nil + } + + userId := auth_session.GetUserId(sessionID.Value, userIP) + + user := &users.User{Id: userId} + err = connection.DB.Select(user) + + if err != nil { + return nil + } + + return user +} + +func getIP(r *http.Request) string { + forwarded := r.Header.Get("X-FORWARDED-FOR") + if forwarded != "" { + return strings.Split(forwarded, ":")[0] + } + return strings.Split(r.RemoteAddr, ":")[0] +} diff --git a/pkg/app/handler/authentication/webauthn/login.go b/pkg/app/handler/authentication/webauthn/login.go new file mode 100644 index 0000000..7bf9c1d --- /dev/null +++ b/pkg/app/handler/authentication/webauthn/login.go @@ -0,0 +1,118 @@ +package webauthn + +import ( + "glsamaker/pkg/app/handler/authentication/auth_session" + "glsamaker/pkg/app/handler/authentication/utils" + "encoding/json" + "fmt" + "github.com/duo-labs/webauthn.io/session" + webauthn_lib "github.com/duo-labs/webauthn/webauthn" + "log" + "net/http" +) + +var ( + WebAuthn *webauthn_lib.WebAuthn + SessionStore *session.Store +) + + +func BeginLogin(w http.ResponseWriter, r *http.Request) { + + user := utils.GetAuthenticatedUser(r) + + CreateWebAuthn() + CreateSessionStore() + + // user doesn't exist + if user == nil { + log.Println("Error fetching the user.") + JsonResponse(w, "Error fetching the user.", http.StatusBadRequest) + return + } + + // generate PublicKeyCredentialRequestOptions, session data + options, sessionData, err := WebAuthn.BeginLogin(user) + if err != nil { + log.Println(err) + JsonResponse(w, err.Error(), http.StatusInternalServerError) + return + } + + // store session data as marshaled JSON + err = SessionStore.SaveWebauthnSession("authentication", sessionData, r, w) + if err != nil { + log.Println(err) + JsonResponse(w, err.Error(), http.StatusInternalServerError) + return + } + + JsonResponse(w, options, http.StatusOK) +} + +func FinishLogin(w http.ResponseWriter, r *http.Request) { + + user := utils.GetAuthenticatedUser(r) + + // user doesn't exist + if user == nil { + log.Println("Error fetching the user.") + JsonResponse(w, "Error fetching the user.", http.StatusBadRequest) + return + } + + // load the session data + sessionData, err := SessionStore.GetWebauthnSession("authentication", r) + if err != nil { + log.Println(err) + JsonResponse(w, err.Error(), http.StatusBadRequest) + return + } + + // in an actual implementation, we should perform additional checks on + // the returned 'credential', i.e. check 'credential.Authenticator.CloneWarning' + // and then increment the credentials counter + _, err = WebAuthn.FinishLogin(user, sessionData, r) + if err != nil { + log.Println(err) + JsonResponse(w, err.Error(), http.StatusBadRequest) + return + } + + // handle successful login + // TODO handle bindLoginToIP correctly + auth_session.Create(w, r, user, true, false) + JsonResponse(w, "Login Success", http.StatusOK) +} + +// from: https://github.com/duo-labs/webauthn.io/blob/3f03b482d21476f6b9fb82b2bf1458ff61a61d41/server/response.go#L15 +func JsonResponse(w http.ResponseWriter, d interface{}, c int) { + dj, err := json.Marshal(d) + if err != nil { + http.Error(w, "Error creating JSON response", http.StatusInternalServerError) + } + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(c) + fmt.Fprintf(w, "%s", dj) +} + +func CreateWebAuthn() { + + if WebAuthn == nil { + authn, _ := webauthn_lib.New(&webauthn_lib.Config{ + RPDisplayName: "Gentoo GLSAMaker", // Display Name for your site + RPID: "glsamakertest.gentoo.org", // Generally the domain name for your site + RPOrigin: "https://glsamakertest.gentoo.org", // The origin URL for WebAuthn requests + RPIcon: "https://assets.gentoo.org/tyrian/site-logo.png", // Optional icon URL for your site + }) + + WebAuthn = authn + } + +} + +func CreateSessionStore() { + if SessionStore == nil { + SessionStore, _ = session.NewStore() + } +} diff --git a/pkg/app/handler/authentication/webauthn/register.go b/pkg/app/handler/authentication/webauthn/register.go new file mode 100644 index 0000000..4e299b3 --- /dev/null +++ b/pkg/app/handler/authentication/webauthn/register.go @@ -0,0 +1,111 @@ +package webauthn + +import ( + "glsamaker/pkg/app/handler/authentication/utils" + "glsamaker/pkg/database/connection" + "glsamaker/pkg/logger" + "fmt" + "github.com/duo-labs/webauthn/protocol" + "log" + "net/http" +) + +func BeginRegistration(w http.ResponseWriter, r *http.Request) { + user := utils.GetAuthenticatedUser(r) + + CreateWebAuthn() + CreateSessionStore() + + if user == nil { + JsonResponse(w, fmt.Errorf("must supply a valid username i.e. foo@bar.com"), http.StatusBadRequest) + return + } + + registerOptions := func(credCreationOpts *protocol.PublicKeyCredentialCreationOptions) { + credCreationOpts.CredentialExcludeList = user.CredentialExcludeList() + } + + // generate PublicKeyCredentialCreationOptions, session data + //var options *protocol.CredentialCreation + //var err error + options, sessionData, err := WebAuthn.BeginRegistration( + user, + registerOptions, + ) + + if err != nil { + log.Println("Error begin register") + log.Println(err) + JsonResponse(w, err.Error(), http.StatusInternalServerError) + return + } + + // store session data as marshaled JSON + err = SessionStore.SaveWebauthnSession("registration", sessionData, r, w) + if err != nil { + log.Println("Error store session") + log.Println(err) + JsonResponse(w, err.Error(), http.StatusInternalServerError) + return + } + + JsonResponse(w, options, http.StatusOK) +} + +func FinishRegistration(w http.ResponseWriter, r *http.Request) { + + authname := getParams(r) + user := utils.GetAuthenticatedUser(r) + + if user == nil { + JsonResponse(w, "Cannot find User", http.StatusBadRequest) + return + } + + // load the session data + sessionData, err := SessionStore.GetWebauthnSession("registration", r) + if err != nil { + log.Println("Error loading session") + log.Println(err) + JsonResponse(w, err.Error(), http.StatusBadRequest) + return + } + + credential, err := WebAuthn.FinishRegistration(user, sessionData, r) + if err != nil { + log.Println("Error finish session") + log.Println(err) + JsonResponse(w, err.Error(), http.StatusBadRequest) + return + } + + user.AddCredential(*credential, authname) + + _, err = connection.DB.Model(user).Column("webauthn_credentials").WherePK().Update() + _, err = connection.DB.Model(user).Column("webauthn_credential_names").WherePK().Update() + if err != nil { + logger.Error.Println("Error adding WebAuthn credentials.") + logger.Error.Println(err) + } + + JsonResponse(w, "Registration Success", http.StatusOK) +} + +func getParams(r *http.Request) string { + + keys, ok := r.URL.Query()["name"] + + if !ok || len(keys[0]) < 1 { + logger.Info.Println("Url Param 'name' is missing") + return "Unnamed Authenticator" + } + + // we only want the single item. + key := keys[0] + + if len(key) > 20 { + key = key[0:20] + } + + return key +} |