package handlers
import (
"fmt"
"html/template"
"net"
"net/http"
"net/url"
"strings"
"time"
"github.com/mk6i/open-oscar-server/state"
"github.com/mk6i/open-oscar-server/wire"
)
const loginPSPCookieMaxAge = 86400
var loginPSPPage = template.Must(template.New("login.psp").Parse(`
Sign in to AIM
`))
type loginPSPPageData struct {
Error string
LoginID string
DevID string
SupportedIDType string
SuccURL string
R string
}
// LoginPSP handles GET and POST /_cqr/login/login.psp for Web AIM SSO login.
func (h *AuthHandler) LoginPSP(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case http.MethodGet:
h.renderLoginPSP(w, r, loginPSPPageData{
DevID: r.URL.Query().Get("devId"),
SupportedIDType: r.URL.Query().Get("supportedIdType"),
SuccURL: r.URL.Query().Get("succUrl"),
R: r.URL.Query().Get("r"),
})
case http.MethodPost:
if err := r.ParseForm(); err != nil {
http.Error(w, "invalid form", http.StatusBadRequest)
return
}
loginID := strings.TrimSpace(r.FormValue("loginId"))
if loginID == "" {
loginID = strings.TrimSpace(r.FormValue("s"))
}
password := r.FormValue("password")
if password == "" {
password = r.FormValue("pwd")
}
data := loginPSPPageData{
LoginID: loginID,
DevID: r.FormValue("devId"),
SupportedIDType: r.FormValue("supportedIdType"),
SuccURL: r.FormValue("succUrl"),
R: r.FormValue("r"),
}
if loginID == "" || password == "" {
data.Error = "Screen name and password are required."
h.renderLoginPSP(w, r, data)
return
}
if err := h.authenticateCredentials(r, loginID, password); err != nil {
h.Logger.DebugContext(r.Context(), "login.psp failed", "loginId", loginID, "error", err)
data.Error = "Invalid screen name or password."
h.renderLoginPSP(w, r, data)
return
}
screenName := state.DisplayScreenName(loginID)
h.setLoginPSPCookies(w, screenName)
redirectURL := safeLoginRedirectURL(r, data.SuccURL)
h.Logger.InfoContext(r.Context(), "login.psp succeeded", "loginId", screenName, "redirect", redirectURL)
http.Redirect(w, r, redirectURL, http.StatusFound)
default:
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
}
}
func (h *AuthHandler) renderLoginPSP(w http.ResponseWriter, r *http.Request, data loginPSPPageData) {
if data.SuccURL == "" {
data.SuccURL = defaultLoginSuccURL(r)
}
w.Header().Set("Content-Type", "text/html; charset=utf-8")
if err := loginPSPPage.Execute(w, data); err != nil {
h.Logger.ErrorContext(r.Context(), "failed to render login.psp", "error", err)
http.Error(w, "internal server error", http.StatusInternalServerError)
}
}
func (h *AuthHandler) authenticateCredentials(r *http.Request, username, password string) error {
signonFrame := wire.FLAPSignonFrame{}
signonFrame.Append(wire.NewTLVBE(wire.LoginTLVTagsScreenName, username))
signonFrame.Append(wire.NewTLVBE(wire.LoginTLVTagsPlaintextPassword, password))
signonFrame.Append(wire.NewTLVBE(wire.LoginTLVTagsMultiConnFlags, wire.MultiConnFlagsRecentClient))
block, err := h.AuthService.FLAPLogin(r.Context(), signonFrame, "")
if err != nil {
return err
}
if block.HasTag(wire.LoginTLVTagsErrorSubcode) {
return fmt.Errorf("login failed")
}
return nil
}
func (h *AuthHandler) setLoginPSPCookies(w http.ResponseWriter, screenName state.DisplayScreenName) {
loginID := string(screenName)
expires := time.Now().Add(loginPSPCookieMaxAge * time.Second)
cookie := func(name, value string) *http.Cookie {
return &http.Cookie{
Name: name,
Value: value,
Path: "/",
Expires: expires,
MaxAge: loginPSPCookieMaxAge,
HttpOnly: false,
SameSite: http.SameSiteLaxMode,
}
}
http.SetCookie(w, cookie("RSP_USER", loginID))
http.SetCookie(w, cookie("RSP_LOCAL", loginID))
http.SetCookie(w, cookie("localAuthUser", loginID+"||"+loginID))
}
// clearLoginPSPCookies expires the SSO cookies set by setLoginPSPCookies (plus
// the oldAimToken cookie honored by getToken) so the browser is logged out.
func (h *AuthHandler) clearLoginPSPCookies(w http.ResponseWriter) {
for _, name := range []string{"RSP_USER", "RSP_LOCAL", "localAuthUser", "oldAimToken"} {
http.SetCookie(w, &http.Cookie{
Name: name,
Value: "",
Path: "/",
Expires: time.Unix(0, 0),
MaxAge: -1,
HttpOnly: false,
SameSite: http.SameSiteLaxMode,
})
}
}
func defaultLoginSuccURL(r *http.Request) string {
scheme := "http"
if r.TLS != nil {
scheme = "https"
}
return scheme + "://" + r.Host + "/"
}
func safeLoginRedirectURL(r *http.Request, succURL string) string {
succURL = strings.TrimSpace(succURL)
if succURL == "" {
return defaultLoginSuccURL(r)
}
target, err := url.Parse(succURL)
if err != nil {
return defaultLoginSuccURL(r)
}
if target.Host == "" {
return succURL
}
reqHost := hostnameOnly(r.Host)
targetHost := hostnameOnly(target.Host)
if targetHost == reqHost || targetHost == "localhost" || targetHost == "127.0.0.1" {
return succURL
}
return defaultLoginSuccURL(r)
}
func hostnameOnly(hostport string) string {
host, _, err := net.SplitHostPort(hostport)
if err != nil {
return hostport
}
return host
}