mirror of https://github.com/dexidp/dex.git
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
190 lines
4.4 KiB
190 lines
4.4 KiB
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. |
|
} |
|
}
|
|
|