2022-04-25 23:41:55 +00:00
|
|
|
// Copyright 2022 the Pinniped contributors. All Rights Reserved.
|
|
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
|
|
|
|
|
|
package login
|
|
|
|
|
|
|
|
import (
|
|
|
|
"net/http"
|
2022-04-29 23:01:51 +00:00
|
|
|
"net/url"
|
2022-04-26 22:30:39 +00:00
|
|
|
|
|
|
|
idpdiscoveryv1alpha1 "go.pinniped.dev/generated/latest/apis/supervisor/idpdiscovery/v1alpha1"
|
|
|
|
"go.pinniped.dev/internal/httputil/httperr"
|
|
|
|
"go.pinniped.dev/internal/httputil/securityheader"
|
|
|
|
"go.pinniped.dev/internal/oidc"
|
2022-05-05 20:12:06 +00:00
|
|
|
"go.pinniped.dev/internal/oidc/login/loginhtml"
|
2022-05-03 23:46:09 +00:00
|
|
|
"go.pinniped.dev/internal/oidc/provider/formposthtml"
|
2022-04-26 22:30:39 +00:00
|
|
|
"go.pinniped.dev/internal/plog"
|
2022-04-25 23:41:55 +00:00
|
|
|
)
|
|
|
|
|
2022-04-29 23:01:51 +00:00
|
|
|
type ErrorParamValue string
|
|
|
|
|
|
|
|
const (
|
|
|
|
usernameParamName = "username"
|
|
|
|
passwordParamName = "password"
|
|
|
|
stateParamName = "state"
|
|
|
|
errParamName = "err"
|
|
|
|
|
|
|
|
ShowNoError ErrorParamValue = ""
|
|
|
|
ShowInternalError ErrorParamValue = "internal_error"
|
|
|
|
ShowBadUserPassErr ErrorParamValue = "login_error"
|
|
|
|
)
|
|
|
|
|
2022-04-26 22:30:39 +00:00
|
|
|
// HandlerFunc is a function that can handle either a GET or POST request for the login endpoint.
|
|
|
|
type HandlerFunc func(
|
|
|
|
w http.ResponseWriter,
|
|
|
|
r *http.Request,
|
|
|
|
encodedState string,
|
|
|
|
decodedState *oidc.UpstreamStateParamData,
|
|
|
|
) error
|
|
|
|
|
|
|
|
// NewHandler returns a http.Handler that serves the login endpoint for IDPs that don't have their own web UI for login.
|
|
|
|
//
|
|
|
|
// This handler takes care of the shared concerns between the GET and POST methods of the login endpoint:
|
|
|
|
// checking the method, checking the CSRF cookie, decoding the state param, and adding security headers.
|
|
|
|
// Then it defers the rest of the handling to the passed in handler functions for GET and POST requests.
|
|
|
|
// Note that CSRF protection isn't needed on GET requests, but it doesn't hurt. Putting it here
|
|
|
|
// keeps the implementations and tests of HandlerFunc simpler since they won't need to deal with any decoders.
|
|
|
|
// Users should always initially get redirected to this page from the authorization endpoint, and never need
|
|
|
|
// to navigate directly to this page in their browser without going through the authorization endpoint first.
|
|
|
|
// Once their browser has landed on this page, it should be okay for the user to refresh the browser.
|
|
|
|
func NewHandler(
|
|
|
|
stateDecoder oidc.Decoder,
|
|
|
|
cookieDecoder oidc.Decoder,
|
|
|
|
getHandler HandlerFunc, // use NewGetHandler() for production
|
|
|
|
postHandler HandlerFunc, // use NewPostHandler() for production
|
|
|
|
) http.Handler {
|
|
|
|
loginHandler := httperr.HandlerFunc(func(w http.ResponseWriter, r *http.Request) error {
|
|
|
|
var handler HandlerFunc
|
|
|
|
switch r.Method {
|
|
|
|
case http.MethodGet:
|
|
|
|
handler = getHandler
|
|
|
|
case http.MethodPost:
|
|
|
|
handler = postHandler
|
|
|
|
default:
|
|
|
|
return httperr.Newf(http.StatusMethodNotAllowed, "%s (try GET or POST)", r.Method)
|
2022-04-25 23:41:55 +00:00
|
|
|
}
|
2022-04-26 22:30:39 +00:00
|
|
|
|
|
|
|
encodedState, decodedState, err := oidc.ReadStateParamAndValidateCSRFCookie(r, cookieDecoder, stateDecoder)
|
2022-04-25 23:41:55 +00:00
|
|
|
if err != nil {
|
2022-04-26 22:30:39 +00:00
|
|
|
plog.InfoErr("state or CSRF error", err)
|
|
|
|
return err
|
2022-04-25 23:41:55 +00:00
|
|
|
}
|
2022-04-26 22:30:39 +00:00
|
|
|
|
|
|
|
switch decodedState.UpstreamType {
|
|
|
|
case string(idpdiscoveryv1alpha1.IDPTypeLDAP), string(idpdiscoveryv1alpha1.IDPTypeActiveDirectory):
|
|
|
|
// these are the types supported by this endpoint, so no error here
|
|
|
|
default:
|
|
|
|
return httperr.Newf(http.StatusBadRequest, "not a supported upstream IDP type for this endpoint: %q", decodedState.UpstreamType)
|
|
|
|
}
|
|
|
|
|
|
|
|
return handler(w, r, encodedState, decodedState)
|
2022-04-25 23:41:55 +00:00
|
|
|
})
|
2022-04-26 22:30:39 +00:00
|
|
|
|
2022-05-03 23:46:09 +00:00
|
|
|
return wrapSecurityHeaders(loginHandler)
|
|
|
|
}
|
|
|
|
|
|
|
|
func wrapSecurityHeaders(handler http.Handler) http.Handler {
|
|
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
2022-05-05 20:12:06 +00:00
|
|
|
wrapped := securityheader.WrapWithCustomCSP(handler, loginhtml.ContentSecurityPolicy())
|
2022-05-04 19:12:14 +00:00
|
|
|
if r.Method == http.MethodPost {
|
2022-05-03 23:46:09 +00:00
|
|
|
// POST requests can result in the form_post html page, so allow it with CSP headers.
|
|
|
|
wrapped = securityheader.WrapWithCustomCSP(handler, formposthtml.ContentSecurityPolicy())
|
|
|
|
}
|
|
|
|
wrapped.ServeHTTP(w, r)
|
|
|
|
})
|
2022-04-25 23:41:55 +00:00
|
|
|
}
|
2022-04-29 23:01:51 +00:00
|
|
|
|
2022-05-19 23:02:08 +00:00
|
|
|
// RedirectToLoginPage redirects to the GET /login page of the specified issuer.
|
|
|
|
// The specified issuer should never end with a "/", which is validated by
|
|
|
|
// provider.FederationDomainIssuer when the issuer string comes from that type.
|
2022-04-29 23:01:51 +00:00
|
|
|
func RedirectToLoginPage(
|
|
|
|
r *http.Request,
|
|
|
|
w http.ResponseWriter,
|
|
|
|
downstreamIssuer string,
|
|
|
|
encodedStateParamValue string,
|
|
|
|
errToDisplay ErrorParamValue,
|
|
|
|
) error {
|
|
|
|
loginURL, err := url.Parse(downstreamIssuer + oidc.PinnipedLoginPath)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
q := loginURL.Query()
|
|
|
|
q.Set(stateParamName, encodedStateParamValue)
|
|
|
|
if errToDisplay != ShowNoError {
|
|
|
|
q.Set(errParamName, string(errToDisplay))
|
|
|
|
}
|
|
|
|
loginURL.RawQuery = q.Encode()
|
|
|
|
|
|
|
|
http.Redirect(w, r,
|
|
|
|
loginURL.String(),
|
|
|
|
http.StatusSeeOther, // match fosite and https://tools.ietf.org/id/draft-ietf-oauth-security-topics-18.html#section-4.11
|
|
|
|
)
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|