Files
opencloud/vendor/github.com/libregraph/lico/identifier/handlers.go
2023-04-19 20:24:34 +02:00

517 lines
15 KiB
Go

/*
* Copyright 2017-2019 Kopano and its licensors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package identifier
import (
"encoding/json"
"encoding/xml"
"fmt"
"net/http"
"net/url"
"strings"
"time"
"github.com/sirupsen/logrus"
"github.com/libregraph/lico/identity/authorities"
"github.com/libregraph/lico/utils"
)
func (i *Identifier) staticHandler(handler http.Handler, cache bool) http.Handler {
return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
addCommonResponseHeaders(rw.Header())
if cache {
rw.Header().Set("Cache-Control", "max-age=3153600, public")
} else {
rw.Header().Set("Cache-Control", "no-cache, max-age=0, public")
}
if strings.HasSuffix(req.URL.Path, "/") {
// Do not serve folder-ish resources.
i.ErrorPage(rw, http.StatusNotFound, "", "")
return
}
handler.ServeHTTP(rw, req)
})
}
func (i *Identifier) secureHandler(handler http.Handler) http.Handler {
return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
var err error
// TODO(longsleep): Add support for X-Forwareded-Host with trusted proxy.
// NOTE: this does not protect from DNS rebinding. Protection for that
// should be added at the frontend proxy.
requiredHost := req.Host
// This follows https://www.owasp.org/index.php/Cross-Site_Request_Forgery_(CSRF)_Prevention_Cheat_Sheet
for {
if req.Header.Get("Kopano-Konnect-XSRF") != "1" {
err = fmt.Errorf("missing xsrf header")
break
}
origin := req.Header.Get("Origin")
referer := req.Header.Get("Referer")
// Require either Origin and Referer header.
// NOTE(longsleep): Firefox does not send Origin header for POST
// requests when on the same domain - this is fuck (tm). See
// https://bugzilla.mozilla.org/show_bug.cgi?id=446344 for reference.
if origin == "" && referer == "" {
err = fmt.Errorf("missing origin or referer header")
break
}
if origin != "" {
originURL, urlParseErr := url.Parse(origin)
if urlParseErr != nil {
err = fmt.Errorf("invalid origin value: %v", urlParseErr)
break
}
if originURL.Host != requiredHost {
err = fmt.Errorf("origin does not match request URL")
break
}
} else if referer != "" {
refererURL, urlParseErr := url.Parse(referer)
if urlParseErr != nil {
err = fmt.Errorf("invalid referer value: %v", urlParseErr)
break
}
if refererURL.Host != requiredHost {
err = fmt.Errorf("referer does not match request URL")
break
}
} else {
i.logger.WithFields(logrus.Fields{
"host": requiredHost,
"user-agent": req.UserAgent(),
}).Warn("identifier HTTP request is insecure with no Origin and Referer")
}
handler.ServeHTTP(rw, req)
return
}
if err != nil {
i.logger.WithError(err).WithFields(logrus.Fields{
"host": requiredHost,
"referer": req.Referer(),
"user-agent": req.UserAgent(),
"origin": req.Header.Get("Origin"),
}).Warn("rejecting identifier HTTP request")
}
i.ErrorPage(rw, http.StatusBadRequest, "", "")
})
}
func (i *Identifier) handleIdentifier(rw http.ResponseWriter, req *http.Request) {
addCommonResponseHeaders(rw.Header())
addNoCacheResponseHeaders(rw.Header())
err := req.ParseForm()
if err != nil {
i.logger.WithError(err).Debugln("identifier failed to decode request")
i.ErrorPage(rw, http.StatusBadRequest, "", "failed to decode request")
return
}
switch req.Form.Get("flow") {
case FlowOIDC, FlowOAuth, "":
if req.Form.Get("identifier") != MustBeSignedIn {
// Check if there is a default authority, if so use that.
authority := i.authorities.Default(req.Context())
if authority != nil {
switch authority.AuthorityType {
case authorities.AuthorityTypeOIDC:
i.writeOAuth2Start(rw, req, authority)
case authorities.AuthorityTypeSAML2:
i.writeSAML2Start(rw, req, authority)
default:
i.ErrorPage(rw, http.StatusNotImplemented, "", "unknown authority type")
}
return
}
}
}
// Show default.
i.writeWebappIndexHTML(rw, req)
}
func (i *Identifier) handleLogon(rw http.ResponseWriter, req *http.Request) {
decoder := json.NewDecoder(req.Body)
var r LogonRequest
err := decoder.Decode(&r)
if err != nil {
i.logger.WithError(err).Debugln("identifier failed to decode logon request")
i.ErrorPage(rw, http.StatusBadRequest, "", "failed to decode request JSON")
return
}
var user *IdentifiedUser
response := &LogonResponse{
State: r.State,
}
addNoCacheResponseHeaders(rw.Header())
record := &Record{}
if r.Hello != nil {
err = r.Hello.parse()
if err != nil {
i.logger.WithError(err).Debugln("identifier failed to parse logon request hello")
i.ErrorPage(rw, http.StatusBadRequest, "", "failed to parse request values")
return
}
record.HelloRequest = r.Hello
}
req = req.WithContext(NewRecordContext(req.Context(), record))
// Params is an array like this [$username, $password, $mode], defining a
// extensible way to extend login modes over time. The minimal length of
// the params array is 1 with only [$username]. Second field is the password
// but its interpretation depends on the third field ($mode). The rest of the
// fields are mode specific.
params := r.Params
for {
paramSize := len(params)
if paramSize == 0 {
i.ErrorPage(rw, http.StatusBadRequest, "", "params required")
break
}
if paramSize >= 3 && params[1] == "" && params[2] == ModeLogonUsernameEmptyPasswordCookie {
// Special mode to allow when same user is logged in via cookie. This
// is used in the select account page logon flow with empty password.
identifiedUser, cookieErr := i.GetUserFromLogonCookie(req.Context(), req, 0, true)
if cookieErr != nil {
i.logger.WithError(cookieErr).Debugln("identifier failed to decode logon cookie in logon request")
}
if identifiedUser != nil {
if identifiedUser.Username() == params[0] {
user = identifiedUser
break
}
}
}
audience := ""
if r.Hello != nil {
audience = r.Hello.ClientID
}
if paramSize < 3 {
// Unsupported logon mode.
break
}
if params[1] == "" {
// Empty password, stop here - never allowed in any mode.
break
}
switch params[2] {
case ModeLogonUsernamePassword:
// Username and password validation mode.
logonedUser, logonErr := i.logonUser(req.Context(), audience, params[0], params[1])
if logonErr != nil {
i.logger.WithError(logonErr).Errorln("identifier failed to logon with backend")
i.ErrorPage(rw, http.StatusInternalServerError, "", "failed to logon")
return
}
user = logonedUser
default:
i.logger.Debugln("identifier unknown logon mode: %v", params[2])
}
break
}
if user == nil || user.Subject() == "" {
rw.Header().Set("Kopano-Konnect-State", response.State)
rw.WriteHeader(http.StatusNoContent)
return
}
// Get user meta data.
// TODO(longsleep): This is an additional request to the backend. This
// should be avoided. Best would be if the backend would return everything
// in one shot (TODO in core).
err = i.updateUser(req.Context(), user, nil)
if err != nil {
i.logger.WithError(err).Debugln("identifier failed to update user data in logon request")
}
// Set logon time.
user.logonAt = time.Now()
if r.Hello != nil {
hello, errHello := i.writeHelloResponse(rw, req, r.Hello, user)
if errHello != nil {
i.logger.WithError(errHello).Debugln("rejecting identifier logon request")
i.ErrorPage(rw, http.StatusBadRequest, "", errHello.Error())
return
}
if !hello.Success {
rw.Header().Set("Kopano-Konnect-State", response.State)
rw.WriteHeader(http.StatusNoContent)
return
}
response.Hello = hello
}
err = i.SetUserToLogonCookie(req.Context(), rw, user)
if err != nil {
i.logger.WithError(err).Errorln("failed to serialize logon ticket")
i.ErrorPage(rw, http.StatusInternalServerError, "", "failed to serialize logon ticket")
return
}
response.Success = true
err = utils.WriteJSON(rw, http.StatusOK, response, "")
if err != nil {
i.logger.WithError(err).Errorln("logon request failed writing response")
}
}
func (i *Identifier) handleLogoff(rw http.ResponseWriter, req *http.Request) {
decoder := json.NewDecoder(req.Body)
var r StateRequest
err := decoder.Decode(&r)
if err != nil {
i.logger.WithError(err).Debugln("identifier failed to decode logoff request")
i.ErrorPage(rw, http.StatusBadRequest, "", "failed to decode request JSON")
return
}
addNoCacheResponseHeaders(rw.Header())
ctx := req.Context()
u, err := i.GetUserFromLogonCookie(ctx, req, 0, false)
if err != nil {
i.logger.WithError(err).Warnln("identifier logoff failed to get logon from ticket")
}
err = i.UnsetLogonCookie(ctx, u, rw)
if err != nil {
i.logger.WithError(err).Errorln("identifier failed to set logoff ticket")
i.ErrorPage(rw, http.StatusInternalServerError, "", "failed to set logoff ticket")
return
}
response := &StateResponse{
State: r.State,
Success: true,
}
err = utils.WriteJSON(rw, http.StatusOK, response, "")
if err != nil {
i.logger.WithError(err).Errorln("logoff request failed writing response")
}
}
func (i *Identifier) handleConsent(rw http.ResponseWriter, req *http.Request) {
decoder := json.NewDecoder(req.Body)
var r ConsentRequest
err := decoder.Decode(&r)
if err != nil {
i.logger.WithError(err).Debugln("identifier failed to decode consent request")
i.ErrorPage(rw, http.StatusBadRequest, "", "failed to decode request JSON")
return
}
addNoCacheResponseHeaders(rw.Header())
consent := &Consent{
Allow: r.Allow,
}
if r.Allow {
consent.RawScope = r.RawScope
}
err = i.SetConsentToConsentCookie(req.Context(), rw, &r, consent)
if err != nil {
i.logger.WithError(err).Errorln("failed to serialize consent ticket")
i.ErrorPage(rw, http.StatusInternalServerError, "", "failed to serialize consent ticket")
return
}
if !r.Allow {
rw.Header().Set("Kopano-Konnect-State", r.State)
rw.WriteHeader(http.StatusNoContent)
return
}
response := &StateResponse{
State: r.State,
Success: true,
}
err = utils.WriteJSON(rw, http.StatusOK, response, "")
if err != nil {
i.logger.WithError(err).Errorln("logoff request failed writing response")
}
}
func (i *Identifier) handleHello(rw http.ResponseWriter, req *http.Request) {
decoder := json.NewDecoder(req.Body)
var r HelloRequest
err := decoder.Decode(&r)
if err != nil {
i.logger.WithError(err).Debugln("identifier failed to decode hello request")
i.ErrorPage(rw, http.StatusBadRequest, "", "failed to decode request JSON")
return
}
err = r.parse()
if err != nil {
i.logger.WithError(err).Debugln("identifier failed to parse hello request")
i.ErrorPage(rw, http.StatusBadRequest, "", "failed to parse request values")
return
}
addNoCacheResponseHeaders(rw.Header())
response, err := i.writeHelloResponse(rw, req, &r, nil)
if err != nil {
i.logger.WithError(err).Debugln("rejecting identifier hello request")
i.ErrorPage(rw, http.StatusBadRequest, "", err.Error())
return
}
err = utils.WriteJSON(rw, http.StatusOK, response, "")
if err != nil {
i.logger.WithError(err).Errorln("hello request failed writing response")
}
}
func (i *Identifier) handleTrampolin(rw http.ResponseWriter, req *http.Request) {
if !strings.HasSuffix(req.URL.Path, ".js") {
err := req.ParseForm()
if err != nil {
i.logger.WithError(err).Debugln("identifier failed to decode trampolin request")
i.ErrorPage(rw, http.StatusBadRequest, "", "failed to decode request parameters")
return
}
sd, err := i.GetStateFromStateCookie(req.Context(), rw, req, "trampolin", req.Form.Get("state"))
if err != nil {
i.ErrorPage(rw, http.StatusBadRequest, "", err.Error())
return
}
if sd == nil || sd.Trampolin == nil {
i.ErrorPage(rw, http.StatusBadRequest, "", "no state")
return
}
scope := sd.Trampolin.Scope
uri, _ := url.Parse(sd.Trampolin.URI)
sd.Trampolin = nil
err = i.SetStateToStateCookie(req.Context(), rw, scope, sd)
if err != nil {
i.logger.WithError(err).Errorln("failed to write trampolin state cookie")
i.ErrorPage(rw, http.StatusInternalServerError, "", "failed to write trampolin state cookie")
return
}
i.writeTrampolinHTML(rw, req, uri)
} else {
i.writeTrampolinScript(rw, req)
}
}
func (i *Identifier) handleOAuth2Start(rw http.ResponseWriter, req *http.Request) {
err := req.ParseForm()
if err != nil {
i.logger.WithError(err).Debugln("identifier failed to decode oauth2 start request")
i.ErrorPage(rw, http.StatusBadRequest, "", "failed to decode request parameters")
return
}
var authority *authorities.Details
if authorityID := req.Form.Get("authority_id"); authorityID != "" {
authority, _ = i.authorities.Lookup(req.Context(), authorityID)
}
i.writeOAuth2Start(rw, req, authority)
}
func (i *Identifier) handleOAuth2Cb(rw http.ResponseWriter, req *http.Request) {
err := req.ParseForm()
if err != nil {
i.logger.WithError(err).Debugln("identifier failed to decode oauth2 cb request")
i.ErrorPage(rw, http.StatusBadRequest, "", "failed to decode request parameters")
return
}
i.writeOAuth2Cb(rw, req)
}
func (i *Identifier) handleSAML2Metadata(rw http.ResponseWriter, req *http.Request) {
authorityDetails := i.authorities.Default(req.Context())
if authorityDetails == nil || authorityDetails.AuthorityType != authorities.AuthorityTypeSAML2 {
i.ErrorPage(rw, http.StatusNotFound, "", "saml not configured")
return
}
metadata := authorityDetails.Metadata()
if metadata == nil {
i.ErrorPage(rw, http.StatusNotFound, "", "saml has no meta data")
return
}
buf, _ := xml.MarshalIndent(metadata, "", " ")
rw.Header().Set("Content-Type", "application/samlmetadata+xml")
rw.WriteHeader(http.StatusOK)
rw.Write([]byte(xml.Header))
rw.Write(buf)
}
func (i *Identifier) handleSAML2AssertionConsumerService(rw http.ResponseWriter, req *http.Request) {
err := req.ParseForm()
if err != nil {
i.logger.WithError(err).Debugln("identifier failed to decode saml2 acs request")
i.ErrorPage(rw, http.StatusBadRequest, "", "failed to decode request parameters")
return
}
i.writeSAML2AssertionConsumerService(rw, req)
}
func (i *Identifier) handleSAML2SingleLogoutService(rw http.ResponseWriter, req *http.Request) {
err := req.ParseForm()
if err != nil {
i.logger.WithError(err).Debugln("identifier failed to decode saml2 slo request")
i.ErrorPage(rw, http.StatusBadRequest, "", "failed to decode request parameters")
return
}
if _, ok := req.Form["SAMLRequest"]; ok {
i.writeSAMLSingleLogoutServiceRequest(rw, req)
} else if _, ok := req.Form["SAMLResponse"]; ok {
i.writeSAMLSingleLogoutServiceResponse(rw, req)
} else {
i.ErrorPage(rw, http.StatusBadRequest, "", "neither SAMLRequest nor SAMLResponse parameter found")
}
}