package main import ( "context" "crypto/rsa" "crypto/x509" "embed" "encoding/base64" "encoding/json" "encoding/pem" "html/template" "io/fs" "log" "math/big" "net/http" "net/url" "github.com/coreos/go-oidc/v3/oidc" ) //go:embed templates/*.html var templatesFS embed.FS //go:embed static/* var staticFS embed.FS const dexLogoDataURI = "/static/dex-glyph-color.svg" var ( indexTmpl *template.Template tokenTmpl *template.Template deviceTmpl *template.Template staticHandler http.Handler ) func init() { var err error indexTmpl, err = template.ParseFS(templatesFS, "templates/index.html") if err != nil { log.Fatalf("failed to parse index template: %v", err) } tokenTmpl, err = template.ParseFS(templatesFS, "templates/token.html") if err != nil { log.Fatalf("failed to parse token template: %v", err) } deviceTmpl, err = template.ParseFS(templatesFS, "templates/device.html") if err != nil { log.Fatalf("failed to parse device template: %v", err) } // Create handler for static files staticSubFS, err := fs.Sub(staticFS, "static") if err != nil { log.Fatalf("failed to create static sub filesystem: %v", err) } staticHandler = http.FileServer(http.FS(staticSubFS)) } func renderIndex(w http.ResponseWriter, data indexPageData) { renderTemplate(w, indexTmpl, data) } func renderDevice(w http.ResponseWriter, data devicePageData) { renderTemplate(w, deviceTmpl, data) } type indexPageData struct { ScopesSupported []string LogoURI string } type devicePageData struct { SessionID string DeviceCode string UserCode string VerificationURI string PollInterval int LogoURI string } type tokenTmplData struct { IDToken string IDTokenJWTLink string AccessToken string AccessTokenJWTLink string RefreshToken string RedirectURL string Claims string PublicKeyPEM string } func generateJWTIOLink(token string, provider *oidc.Provider, ctx context.Context) string { // JWT.io doesn't support automatic public key via URL parameter // The public key is displayed separately on the page for manual copy-paste return "https://jwt.io/#debugger-io?token=" + url.QueryEscape(token) } func getPublicKeyPEM(provider *oidc.Provider) string { if provider == nil { return "" } jwksURL := provider.Endpoint().AuthURL if len(jwksURL) > 5 { jwksURL = jwksURL[:len(jwksURL)-5] + "/keys" } else { return "" } resp, err := http.Get(jwksURL) if err != nil { return "" } defer resp.Body.Close() var jwks struct { Keys []json.RawMessage `json:"keys"` } if err := json.NewDecoder(resp.Body).Decode(&jwks); err != nil || len(jwks.Keys) == 0 { return "" } var key struct { N string `json:"n"` E string `json:"e"` Kty string `json:"kty"` } if err := json.Unmarshal(jwks.Keys[0], &key); err != nil || key.Kty != "RSA" { return "" } nBytes, err1 := base64.RawURLEncoding.DecodeString(key.N) eBytes, err2 := base64.RawURLEncoding.DecodeString(key.E) if err1 != nil || err2 != nil { return "" } var eInt int for _, b := range eBytes { eInt = eInt<<8 | int(b) } pubKey := &rsa.PublicKey{ N: new(big.Int).SetBytes(nBytes), E: eInt, } pubKeyBytes, err := x509.MarshalPKIXPublicKey(pubKey) if err != nil { return "" } pubKeyPEM := pem.EncodeToMemory(&pem.Block{ Type: "PUBLIC KEY", Bytes: pubKeyBytes, }) return string(pubKeyPEM) } func renderToken(w http.ResponseWriter, ctx context.Context, provider *oidc.Provider, redirectURL, idToken, accessToken, refreshToken, claims string) { data := tokenTmplData{ IDToken: idToken, IDTokenJWTLink: generateJWTIOLink(idToken, provider, ctx), AccessToken: accessToken, AccessTokenJWTLink: generateJWTIOLink(accessToken, provider, ctx), RefreshToken: refreshToken, RedirectURL: redirectURL, Claims: claims, PublicKeyPEM: getPublicKeyPEM(provider), } renderTemplate(w, tokenTmpl, data) } func renderTemplate(w http.ResponseWriter, tmpl *template.Template, data interface{}) { err := tmpl.Execute(w, data) if err == nil { return } switch err := err.(type) { case *template.Error: log.Printf("Error rendering template %s: %s", tmpl.Name(), err) http.Error(w, "Internal server error", http.StatusInternalServerError) default: // An error with the underlying write, such as the connection being dropped. Ignore for now. } }