18F/e-QIP-prototype

View on GitHub
api/http/saml.go

Summary

Maintainability
A
0 mins
Test Coverage
package http

import (
    "fmt"
    "net/http"
    "os"

    "github.com/18F/e-QIP-prototype/api"
    "github.com/18F/e-QIP-prototype/api/simplestore"
)

var (
    redirectTo = os.Getenv("API_REDIRECT")
)

// SamlRequestHandler is the handler for creating a SAML request.
type SamlRequestHandler struct {
    Env      api.Settings
    Log      api.LogService
    Database api.DatabaseService
    SAML     api.SamlService
}

// ServeHTTP is the initial entry point for authentication.
func (service SamlRequestHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    if !service.Env.True(api.SamlEnabled) {
        service.Log.Warn(api.SamlNotEnabled, api.LogFields{})
        RespondWithStructuredError(w, api.SamlNotEnabled, http.StatusInternalServerError)
        return
    }

    encoded, url, err := service.SAML.CreateAuthenticationRequest()
    if err != nil {
        service.Log.WarnError(api.SamlRequestError, err, api.LogFields{})
        RespondWithStructuredError(w, api.SamlRequestError, http.StatusInternalServerError)
        return
    }

    EncodeJSON(w, struct {
        Base64XML string
        URL       string
    }{
        encoded,
        url,
    })
}

// SamlSLORequestHandler is the handler for creating a SAML Logout request
type SamlSLORequestHandler struct {
    Env      api.Settings
    Log      api.LogService
    Database api.DatabaseService
    SAML     api.SamlService
    Session  api.SessionService
}

// ServeHTTP is the initial entry point for authentication.
func (service SamlSLORequestHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    if !service.Env.True(api.SamlEnabled) || !service.Env.True(api.SamlSloEnabled) {
        service.Log.Warn(api.SamlSLONotEnabled, api.LogFields{})
        http.Error(w, api.SamlSLONotEnabled, http.StatusInternalServerError)
        return
    }

    account, session := AccountAndSessionFromRequestContext(r)

    if !session.SessionIndex.Valid {
        service.Log.Warn(api.SamlSLOMissingSessionIndex, api.LogFields{})
        http.Error(w, api.SamlSLOMissingSessionIndex, http.StatusInternalServerError)
        return
    }

    sessionIndex := session.SessionIndex.String

    encoded, url, err := service.SAML.CreateSLORequest(account.Username, sessionIndex)
    if err != nil {
        service.Log.WarnError(api.SamlSLORequestGeneration, err, api.LogFields{})
        http.Error(w, api.SamlSLORequestGeneration, http.StatusInternalServerError)
        return
    }

    EncodeJSON(w, struct {
        Base64XML string
        URL       string
    }{
        encoded,
        url,
    })
}

// SamlResponseHandler is the callback handler for both login and logout SAML Responses.
type SamlResponseHandler struct {
    Env      api.Settings
    Log      api.LogService
    Database api.DatabaseService
    SAML     api.SamlService
    Session  api.SessionService
    Cookie   SessionCookieService
}

// ServeHTTP is the callback handler for both login and logout SAML Responses.
func (service SamlResponseHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    if !service.Env.True(api.SamlEnabled) {
        service.Log.Warn(api.SamlNotImplemented, api.LogFields{})
        RespondWithStructuredError(w, api.SamlNotImplemented, http.StatusInternalServerError)
        return
    }

    encoded := r.FormValue("SAMLResponse")
    if encoded == "" {
        service.Log.Warn(api.SamlFormError, api.LogFields{})
        redirectAccessDenied(w, r)
        return
    }

    responseType, err := service.SAML.ResponseType(encoded)
    if err != nil {
        service.Log.WarnError(api.SamlParseError, err, api.LogFields{})
        redirectAccessDenied(w, r)
        return
    }

    switch responseType {
    case api.AuthnSAMLResponseType:
        service.serveAuthnResponse(encoded, w, r)
        return
    case api.LogoutSAMLResponseType:
        service.serveLogoutResponse(encoded, w, r)
        return
    default:
        service.Log.Fatal("SAML.ResponseType returned an unknown response type. This is programmer error due to the lack of go enums", api.LogFields{"unknownResponseType": responseType})
        http.Error(w, "Server Error", http.StatusInternalServerError)
    }
}

func (service SamlResponseHandler) serveAuthnResponse(encodedResponse string, w http.ResponseWriter, r *http.Request) {
    username, sessionIndex, err := service.SAML.ValidateAuthenticationResponse(encodedResponse)
    if err != nil {
        redirectAccessDenied(w, r)
        return
    }

    // Associate with a database context.
    account := &api.Account{
        Username: username,
    }
    if _, err := account.Get(service.Database, account.ID); err != nil {
        service.Log.WarnError(api.NoAccount, err, api.LogFields{"username": username})

        redirectAccessDenied(w, r)
    }

    sessionKey, authErr := service.Session.UserDidAuthenticate(account.ID, simplestore.NonNullString(sessionIndex))
    if authErr != nil {
        service.Log.WarnError("bad session get", authErr, api.LogFields{"account": account.ID})
        RespondWithStructuredError(w, "bad session get", http.StatusInternalServerError)
        return
    }

    service.Cookie.AddSessionKeyToResponse(w, sessionKey)

    http.Redirect(w, r, redirectTo, http.StatusFound)
}

func (service SamlResponseHandler) serveLogoutResponse(encodedResponse string, w http.ResponseWriter, r *http.Request) {
    // TODO: validate the SAML Logout Response and tie it to a session via the SLO request ID

    // We are not wrapped by the session middleware, so we have to check the cookie on our own
    sessionCookie, cookieErr := r.Cookie(SessionCookieName)
    if cookieErr != nil {
        service.Log.WarnError(api.RequestIsMissingSessionCookie, cookieErr, api.LogFields{})
        redirectLogoutFailed(w, r)
        return
    }

    sessionKey := sessionCookie.Value

    logoutErr := service.Session.UserDidLogout(sessionKey)
    if logoutErr != nil {
        service.Log.WarnError(api.SamlSLOLogoutFailed, logoutErr, api.LogFields{})
        redirectLogoutFailed(w, r)
        return
    }

    DeleteSessionCookie(w)

    redirectLogout(w, r)
}

func redirectLogoutFailed(w http.ResponseWriter, r *http.Request) {
    url := fmt.Sprintf("%s?error=saml_logout_failed", redirectTo)
    http.Redirect(w, r, url, http.StatusFound)
}

func redirectAccessDenied(w http.ResponseWriter, r *http.Request) {
    url := fmt.Sprintf("%s?error=access_denied", redirectTo)
    http.Redirect(w, r, url, http.StatusFound)
}

func redirectLogout(w http.ResponseWriter, r *http.Request) {
    http.Redirect(w, r, redirectTo, http.StatusFound)
}