package verify import ( "net/http" "edge-infra.dev/pkg/edge/iam/log" "edge-infra.dev/pkg/edge/iam/verify/templates" "github.com/MicahParks/keyfunc" "github.com/coreos/go-oidc/v3/oidc" "github.com/gin-gonic/gin" "github.com/golang-jwt/jwt/v4" "golang.org/x/oauth2" ) type IDClaims struct { Organization string `json:"org"` } func (v *Verifier) callback(ctx *gin.Context) { log := log.Get(ctx.Request.Context()) result := &Result{ Name: "verify callback result", Pass: true, } // discover openid config to setup a oauth2 client provider, err := oidc.NewProvider(oidc.InsecureIssuerURLContext(ctx, Issuer()), IssuerURL()) if err != nil { err := ctx.AbortWithError(http.StatusInternalServerError, err) if err != nil { log.Error(err, "failed to abort with error") } return } // weirdly named just config, but it's a client as well oauth2Config := oauth2.Config{ ClientID: v.ClientID, ClientSecret: v.ClientSecret, RedirectURL: v.ClientURL + verifyCallbackPath, Endpoint: provider.Endpoint(), Scopes: []string{oidc.ScopeOpenID, oidc.ScopeOfflineAccess, "profile"}, } // would have expected to be able to do this jwksURL := IssuerURL() + wellKnownJWKSPath jwks, err := keyfunc.Get(jwksURL, keyfunc.Options{}) if err != nil { //TODO: replace these repeated 3 lines into `return fail("message")` step := Step{Name: "load JWKS", Pass: false} writeResult(ctx, templates.Callback, result, step) return } // grab the auth code from the URL code, _ := ctx.GetQuery("code") // fetch the tokenset using the auth code tokenSet, err := oauth2Config.Exchange(ctx, code, oauth2.AccessTypeOffline) if err != nil { step := Step{Name: "exchange code for tokens", Pass: false} writeResult(ctx, templates.Callback, result, step) return } // validate it if !tokenSet.Valid() { step := Step{Name: "valid token", Pass: false} writeResult(ctx, templates.Callback, result, step) return } // parse and validate the access token accessToken, err := jwt.Parse(tokenSet.AccessToken, jwks.Keyfunc) if err != nil { step := Step{Name: "parse, validate and verify the token", Pass: false} writeResult(ctx, templates.Callback, result, step) return } // extract the id_token rawIDToken, ok := tokenSet.Extra("id_token").(string) if !ok { step := Step{Name: "token includes and id_token", Pass: false} writeResult(ctx, templates.Callback, result, step) return } // verify and parse the id_token verifier := provider.Verifier(&oidc.Config{ ClientID: v.ClientID, }) //logger.Debug("token", "id-token", rawIDToken) idToken, err := verifier.Verify(ctx, rawIDToken) if err != nil { step := Step{Name: "id_token is valid", Pass: false} writeResult(ctx, templates.Callback, result, step) return } idClaims := IDClaims{} if err := idToken.Claims(&idClaims); err != nil { step := Step{Name: "id_token has valid claims", Pass: false} writeResult(ctx, templates.Callback, result, step) return } claims, _ := accessToken.Claims.(jwt.MapClaims) steps := make([]Step, 0) steps = append(steps, Step{Name: "valid access token", Pass: accessToken.Valid}, Step{Name: "organization is correct", IsPublic: true, Pass: claims["org"] == BslExpectedOrganization(), Expected: BslExpectedOrganization(), Got: claims["org"].(string)}, Step{Name: "subject is correct", IsPublic: true, Pass: claims["sub"] == BslExpectedSubject(), Expected: BslExpectedSubject(), Got: claims["sub"].(string)}, Step{Name: "we have scopes", Pass: claims["scp"] != ""}, Step{Name: "we have roles", Pass: claims["rls"] != ""}, //TODO: [EyalW] - assert also the expected issuer //VerificationStep{Name: "ID Token has correct issuer", Pass: idToken.Issuer == EXPECT_ISSUER}, Step{Name: "id token has correct audience", IsPublic: true, Pass: idToken.Audience[0] == v.ClientID, Expected: v.ClientSecret, Got: idToken.Audience[0]}, Step{Name: "id token has correct subject", IsPublic: true, Pass: idToken.Subject == BslExpectedSubject(), Expected: BslExpectedSubject(), Got: idToken.Subject}, ) writeResult(ctx, templates.Callback, result, steps...) }