From 8ed2ebecf48d4387663f59ce4dd3a1bdcadb0e09 Mon Sep 17 00:00:00 2001 From: Josh Komoroske Date: Fri, 9 May 2025 00:49:05 -0400 Subject: [PATCH] feat: pretty print saml assertion to js console --- server/format.go | 50 ++++++++++++++++++++++++++++++++++++++++++ server/server.go | 37 +++++++++++++++++++++---------- server/static/index.js | 28 ++++++++++------------- 3 files changed, 88 insertions(+), 27 deletions(-) create mode 100644 server/format.go diff --git a/server/format.go b/server/format.go new file mode 100644 index 0000000..c0111f9 --- /dev/null +++ b/server/format.go @@ -0,0 +1,50 @@ +// Copyright Josh Komoroske. All rights reserved. +// Use of this source code is governed by the MIT license, +// a copy of which can be found in the LICENSE.txt file. +// SPDX-License-Identifier: MIT + +package server + +import ( + "bytes" + "encoding/base64" + "encoding/xml" + "io" +) + +// formatSAMLResponse takes a base64 encoded SAML assertion body, base64 +// decodes it, then pretty-prints the contained XML document. +func formatSAMLResponse(raw string, writer io.Writer) error { + decoder := xml.NewDecoder(base64.NewDecoder(base64.StdEncoding, bytes.NewReader([]byte(raw)))) + + encoder := xml.NewEncoder(writer) + encoder.Indent("", " ") + + // We have to round-trip through decoding+(re)encoding every token since it + // isn't possible to unmarshal into an interface{}. + for { + token, err := decoder.RawToken() + + if err == io.EOF { + break + } + + if err != nil { + return err + } + + if err := encoder.EncodeToken(token); err != nil { + return err + } + } + + if err := encoder.Flush(); err != nil { + return err + } + + // Append a single newline character to improve with copying the formatted + // XML body from the console. + _, err := writer.Write([]byte("\n")) + + return err +} diff --git a/server/server.go b/server/server.go index 11f72c2..c829383 100644 --- a/server/server.go +++ b/server/server.go @@ -19,11 +19,13 @@ import ( // Returns a function that must be invoked by the caller to wait for the SAML // response and the server to shutdown. func Start(ctx context.Context, listen, url string) (string, func() (string, error)) { //nolint:cyclop,funlen - // Channels for asynchronously communicating the SAML response string or - // any errors that are encountered. - responseChan := make(chan string) + // Channels for asynchronously communicating any error that is encountered. errChan := make(chan error) + // The SAML response body that is received by the callback handler and + // passed on to AWS. + var samlResponse string + // sendError is a helper function for sending errors to the error channel // in a non-blocking fashion. sendError := func(err error) { @@ -71,13 +73,32 @@ func Start(ctx context.Context, listen, url string) (string, func() (string, err } go func() { - // Write the SAML response string to our channel. - responseChan <- request.FormValue("SAMLResponse") + // Read the SAML response string. + samlResponse = request.FormValue("SAMLResponse") }() http.Redirect(writer, request, "/", http.StatusFound) }) + // The /response route serves the formatted SAML assertion. + mux.HandleFunc("/response", func(writer http.ResponseWriter, request *http.Request) { + if request.Method != http.MethodGet { + http.Error(writer, "method not allowed", http.StatusMethodNotAllowed) + + return + } + + if len(samlResponse) == 0 { + http.NotFound(writer, request) + + return + } + + if err := formatSAMLResponse(samlResponse, writer); err != nil { + http.Error(writer, err.Error(), http.StatusInternalServerError) + } + }) + // The /callback route is called by the user to terminate this server. mux.HandleFunc("/shutdown", func(writer http.ResponseWriter, request *http.Request) { if request.Method != http.MethodPost { @@ -114,12 +135,6 @@ func Start(ctx context.Context, listen, url string) (string, func() (string, err return fmt.Sprintf("http://%s/login", listen), func() (string, error) { defer server.Shutdown(ctx) //nolint:errcheck - var samlResponse string - // Wait for the SAML response. - go func() { - samlResponse = <-responseChan - }() - // Wait for an error. err := <-errChan switch err { diff --git a/server/static/index.js b/server/static/index.js index e257671..4712bce 100644 --- a/server/static/index.js +++ b/server/static/index.js @@ -1,18 +1,14 @@ "use strict"; -// ready calls the given function when the page is loaded and ready. -function ready(fn) { - if (document.readyState !== 'loading') { - fn(); - } else { - document.addEventListener('DOMContentLoaded', fn); - } -} - -// shutdown sends a POST to the shutdown endpoint, signaling to the server to -// terminate itself. -function shutdown() { - fetch('/shutdown', {method: 'POST'}); -} - -ready(shutdown); +window.addEventListener('load', () => { + fetch('/response') + .then(response => response.text()) // send response body to next then chain + .then(body => { + console.groupCollapsed("SAML Assertion"); + console.log(body); + console.groupEnd(); + }) + .finally(() => { + fetch('/shutdown', {method: 'POST'}); + }); +});