// A minimal origin that verifies Loclix assertions before using an identity.
package main

import (
	"context"
	"encoding/json"
	"errors"
	"log"
	"net/http"
	"net/url"
	"os"
	"time"

	"github.com/coreos/go-oidc/v3/oidc"
)

type identity struct {
	Subject       string `json:"sub"`
	Provider      string `json:"provider"`
	Email         string `json:"email"`
	EmailVerified bool   `json:"email_verified"`
	Application   string `json:"application_id"`
	Host          string `json:"host"`
	Method        string `json:"method"`
	URI           string `json:"uri"`
	OriginURI     string `json:"origin_uri"`
	NotBefore     int64  `json:"nbf"`
	IssuedAt      int64  `json:"iat"`
}

func verifyRequest(r *http.Request, verifier *oidc.IDTokenVerifier, host, audience string) (identity, error) {
	var claims identity
	raw := r.Header.Get("X-Loclix-Access-Jwt-Assertion")
	if raw == "" {
		return claims, errors.New("missing access assertion")
	}
	token, err := verifier.Verify(r.Context(), raw)
	if err != nil {
		return claims, err
	}
	if err = token.Claims(&claims); err != nil {
		return claims, err
	}
	now := time.Now()
	if claims.Subject == "" || claims.Provider == "" || claims.Application != audience || claims.Host != host || claims.Method != r.Method || claims.OriginURI != r.URL.RequestURI() || claims.NotBefore == 0 || claims.NotBefore > now.Unix() || claims.IssuedAt == 0 || claims.IssuedAt > now.Add(5*time.Second).Unix() || token.Expiry.After(time.Unix(claims.IssuedAt, 0).Add(time.Minute)) {
		return claims, errors.New("access assertion does not match this request")
	}
	if !claims.EmailVerified {
		claims.Email = ""
	}
	return claims, nil
}
func main() {
	// Read these values from trusted deployment configuration, never from a token.
	issuer, audience := os.Getenv("LOCLIX_ACCESS_ISSUER"), os.Getenv("LOCLIX_ACCESS_AUDIENCE")
	u, err := url.Parse(issuer)
	if err != nil || u.Scheme != "https" || u.Host == "" || u.User != nil || u.Path != "/_loclix/access/" || u.RawQuery != "" || u.Fragment != "" || audience == "" {
		log.Fatal("configure LOCLIX_ACCESS_ISSUER and LOCLIX_ACCESS_AUDIENCE")
	}
	client := &http.Client{Timeout: 5 * time.Second, CheckRedirect: func(*http.Request, []*http.Request) error {
		return errors.New("signing key redirects are not permitted")
	}}
	keys := oidc.NewRemoteKeySet(oidc.ClientContext(context.Background(), client), issuer+"jwks.json")
	verifier := oidc.NewVerifier(issuer, keys, &oidc.Config{ClientID: audience, SupportedSigningAlgs: []string{"RS256"}})
	server := &http.Server{Addr: "127.0.0.1:9090", ReadHeaderTimeout: 5 * time.Second, Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		claims, err := verifyRequest(r, verifier, u.Hostname(), audience)
		w.Header().Set("Cache-Control", "private, no-store")
		if err != nil {
			http.Error(w, "Application access denied", http.StatusUnauthorized)
			return
		}
		// Use the verified subject to look up an application user. An email address
		// is available only when the identity provider verified it.
		w.Header().Set("Content-Type", "application/json")
		_ = json.NewEncoder(w).Encode(map[string]string{"subject": claims.Subject, "email": claims.Email})
	})}
	log.Fatal(server.ListenAndServe())
}
