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

AIM Sign In

Sign in with your Open OSCAR account.

{{if .Error}}
{{.Error}}
{{end}}
`)) 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 }