1
0
mirror of https://github.com/oauth2-proxy/oauth2-proxy.git synced 2024-11-24 08:52:25 +02:00
oauth2-proxy/providers/adfs_test.go

304 lines
8.5 KiB
Go
Raw Permalink Normal View History

2021-06-13 10:00:12 +02:00
package providers
import (
"context"
"crypto/rand"
"crypto/rsa"
2021-06-13 10:00:12 +02:00
"encoding/base64"
"errors"
2021-06-13 10:00:12 +02:00
"net/http"
"net/http/httptest"
"net/url"
"strings"
2021-07-17 18:55:05 +02:00
"github.com/coreos/go-oidc/v3/oidc"
2024-03-04 02:42:00 +02:00
"github.com/golang-jwt/jwt/v5"
"github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/options"
2021-06-13 10:00:12 +02:00
"github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/sessions"
internaloidc "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/providers/oidc"
. "github.com/onsi/ginkgo/v2"
2021-06-13 10:00:12 +02:00
. "github.com/onsi/gomega"
)
type fakeADFSJwks struct{}
func (fakeADFSJwks) VerifySignature(_ context.Context, jwt string) (payload []byte, err error) {
decodeString, err := base64.RawURLEncoding.DecodeString(strings.Split(jwt, ".")[1])
if err != nil {
return nil, err
}
return decodeString, nil
}
type adfsClaims struct {
UPN string `json:"upn,omitempty"`
idTokenClaims
}
func newSignedTestADFSToken(tokenClaims adfsClaims) (string, error) {
key, _ := rsa.GenerateKey(rand.Reader, 2048)
standardClaims := jwt.NewWithClaims(jwt.SigningMethodRS256, tokenClaims)
return standardClaims.SignedString(key)
}
2021-06-13 10:00:12 +02:00
func testADFSProvider(hostname string) *ADFSProvider {
verificationOptions := internaloidc.IDTokenVerificationOptions{
improved audience handling to support client credentials access tokens without aud claims (#1204) * implementation draft * add cfg options skip-au-when-missing && client-id-verification-claim; enhance the provider data verification logic for sake of the added options * refactor configs, added logging and add additional claim verification * simplify logic by just having one configuration similar to oidc-email-claim * added internal oidc token verifier, so that aud check behavior can be managed with oauth2-proxy and is compatible with extra-jwt-issuers * refactored verification to reduce complexity * refactored verification to reduce complexity * added docs * adjust tests to support new OIDCAudienceClaim and OIDCExtraAudiences options * extend unit tests and ensure that audience is set with the value of aud claim configuration * revert filemodes and update docs * update docs * remove unneccesary logging, refactor audience existence check and added additional unit tests * fix linting issues after rebase on origin/main * cleanup: use new imports for migrated libraries after rebase on origin/main * adapt mock in keycloak_oidc_test.go * allow specifying multiple audience claims, fixed bug where jwt issuers client id was not the being considered and fixed bug where aud claims with multiple audiences has broken the whole validation * fixed formatting issue * do not pass the whole options struct to minimize complexity and dependency to the configuration structure * added changelog entry * update docs Co-authored-by: Sofia Weiler <sofia.weiler@aoe.com> Co-authored-by: Christian Zenker <christian.zenker@aoe.com>
2022-02-15 18:12:22 +02:00
AudienceClaims: []string{"aud"},
ClientID: "https://test.myapp.com",
}
o := internaloidc.NewVerifier(oidc.NewVerifier(
2021-06-13 10:00:12 +02:00
"https://issuer.example.com",
fakeADFSJwks{},
&oidc.Config{ClientID: "https://test.myapp.com"},
improved audience handling to support client credentials access tokens without aud claims (#1204) * implementation draft * add cfg options skip-au-when-missing && client-id-verification-claim; enhance the provider data verification logic for sake of the added options * refactor configs, added logging and add additional claim verification * simplify logic by just having one configuration similar to oidc-email-claim * added internal oidc token verifier, so that aud check behavior can be managed with oauth2-proxy and is compatible with extra-jwt-issuers * refactored verification to reduce complexity * refactored verification to reduce complexity * added docs * adjust tests to support new OIDCAudienceClaim and OIDCExtraAudiences options * extend unit tests and ensure that audience is set with the value of aud claim configuration * revert filemodes and update docs * update docs * remove unneccesary logging, refactor audience existence check and added additional unit tests * fix linting issues after rebase on origin/main * cleanup: use new imports for migrated libraries after rebase on origin/main * adapt mock in keycloak_oidc_test.go * allow specifying multiple audience claims, fixed bug where jwt issuers client id was not the being considered and fixed bug where aud claims with multiple audiences has broken the whole validation * fixed formatting issue * do not pass the whole options struct to minimize complexity and dependency to the configuration structure * added changelog entry * update docs Co-authored-by: Sofia Weiler <sofia.weiler@aoe.com> Co-authored-by: Christian Zenker <christian.zenker@aoe.com>
2022-02-15 18:12:22 +02:00
), verificationOptions)
2021-06-13 10:00:12 +02:00
p := NewADFSProvider(&ProviderData{
ProviderName: "",
LoginURL: &url.URL{},
RedeemURL: &url.URL{},
ProfileURL: &url.URL{},
ValidateURL: &url.URL{},
Scope: "",
Verifier: o,
EmailClaim: options.OIDCEmailClaim,
}, options.Provider{})
2021-06-13 10:00:12 +02:00
if hostname != "" {
updateURL(p.Data().LoginURL, hostname)
updateURL(p.Data().RedeemURL, hostname)
updateURL(p.Data().ProfileURL, hostname)
updateURL(p.Data().ValidateURL, hostname)
}
return p
}
func testADFSBackend() *httptest.Server {
authResponse := `
{
"access_token": "my_access_token",
"id_token": "my_id_token",
"refresh_token": "my_refresh_token"
2021-06-13 10:00:12 +02:00
}
`
userInfo := `
{
"email": "samiracho@email.com"
}
`
refreshResponse := `{ "access_token": "new_some_access_token", "refresh_token": "new_some_refresh_token", "expires_in": "32693148245", "id_token": "new_some_id_token" }`
authHeader := "Bearer adfs_access_token"
return httptest.NewServer(http.HandlerFunc(
func(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
case "/adfs/oauth2/authorize":
w.WriteHeader(200)
w.Write([]byte(authResponse))
case "/adfs/oauth2/refresh":
w.WriteHeader(200)
w.Write([]byte(refreshResponse))
case "/adfs/oauth2/userinfo":
if r.Header["Authorization"][0] == authHeader {
w.WriteHeader(200)
w.Write([]byte(userInfo))
} else {
w.WriteHeader(401)
}
default:
w.WriteHeader(200)
}
}))
}
var _ = Describe("ADFS Provider Tests", func() {
var p *ADFSProvider
var b *httptest.Server
BeforeEach(func() {
b = testADFSBackend()
bURL, err := url.Parse(b.URL)
Expect(err).To(BeNil())
p = testADFSProvider(bURL.Host)
})
AfterEach(func() {
b.Close()
})
Context("New Provider Init", func() {
It("uses defaults", func() {
providerData := NewADFSProvider(&ProviderData{}, options.Provider{}).Data()
2021-06-13 10:00:12 +02:00
Expect(providerData.ProviderName).To(Equal("ADFS"))
Expect(providerData.Scope).To(Equal(oidcDefaultScope))
})
It("uses custom scope", func() {
providerData := NewADFSProvider(&ProviderData{Scope: "openid email"}, options.Provider{}).Data()
Expect(providerData.ProviderName).To(Equal("ADFS"))
Expect(providerData.Scope).To(Equal("openid email"))
Expect(providerData.Scope).NotTo(Equal(oidcDefaultScope))
2021-06-13 10:00:12 +02:00
})
})
Context("with bad token", func() {
It("should trigger an error", func() {
session := &sessions.SessionState{AccessToken: "unexpected_adfs_access_token", IDToken: "malformed_token"}
err := p.EnrichSession(context.Background(), session)
Expect(err).NotTo(BeNil())
})
})
Context("with valid token", func() {
It("should not throw an error", func() {
rawIDToken, _ := newSignedTestIDToken(defaultIDToken)
session, err := p.buildSessionFromClaims(rawIDToken, "")
2021-06-13 10:00:12 +02:00
Expect(err).To(BeNil())
session.IDToken = rawIDToken
2021-06-13 10:00:12 +02:00
err = p.EnrichSession(context.Background(), session)
Expect(session.Email).To(Equal("janed@me.com"))
Expect(err).To(BeNil())
})
})
Context("with skipScope enabled", func() {
It("should not include parameter scope", func() {
resource, _ := url.Parse("http://example.com")
p := NewADFSProvider(&ProviderData{
ProtectedResource: resource,
Scope: "",
}, options.Provider{
ADFSConfig: options.ADFSOptions{SkipScope: true},
})
2021-06-13 10:00:12 +02:00
result := p.GetLoginURL("https://example.com/adfs/oauth2/", "", "", url.Values{})
2021-06-13 10:00:12 +02:00
Expect(result).NotTo(ContainSubstring("scope="))
})
})
Context("With resource parameter", func() {
type scopeTableInput struct {
resource string
scope string
expectedScope string
}
DescribeTable("should return expected results",
func(in scopeTableInput) {
resource, _ := url.Parse(in.resource)
p := NewADFSProvider(&ProviderData{
ProtectedResource: resource,
Scope: in.scope,
}, options.Provider{})
2021-06-13 10:00:12 +02:00
Expect(p.Data().Scope).To(Equal(in.expectedScope))
result := p.GetLoginURL("https://example.com/adfs/oauth2/", "", "", url.Values{})
2021-06-13 10:00:12 +02:00
Expect(result).To(ContainSubstring("scope=" + url.QueryEscape(in.expectedScope)))
},
Entry("should add slash", scopeTableInput{
resource: "http://resource.com",
scope: "openid",
expectedScope: "http://resource.com/openid",
}),
Entry("shouldn't add extra slash", scopeTableInput{
resource: "http://resource.com/",
scope: "openid",
expectedScope: "http://resource.com/openid",
}),
Entry("should add default scopes with resource", scopeTableInput{
resource: "http://resource.com/",
scope: "",
expectedScope: "http://resource.com/openid email profile",
}),
Entry("should add default scopes", scopeTableInput{
resource: "",
scope: "",
expectedScope: "openid email profile",
}),
Entry("shouldn't add resource if already in scopes", scopeTableInput{
resource: "http://resource.com",
scope: "http://resource.com/openid",
expectedScope: "http://resource.com/openid",
}),
)
})
Context("UPN Fallback", func() {
var idToken string
var session *sessions.SessionState
BeforeEach(func() {
var err error
idToken, err = newSignedTestADFSToken(adfsClaims{
UPN: "upn@company.com",
idTokenClaims: minimalIDToken,
})
Expect(err).ToNot(HaveOccurred())
session = &sessions.SessionState{
IDToken: idToken,
}
})
Describe("EnrichSession", func() {
It("uses email claim if present", func() {
p.oidcEnrichFunc = func(_ context.Context, s *sessions.SessionState) error {
s.Email = "person@company.com"
return nil
}
err := p.EnrichSession(context.Background(), session)
Expect(err).ToNot(HaveOccurred())
Expect(session.Email).To(Equal("person@company.com"))
})
It("falls back to UPN claim if Email is missing", func() {
p.oidcEnrichFunc = func(_ context.Context, s *sessions.SessionState) error {
return nil
}
err := p.EnrichSession(context.Background(), session)
Expect(err).ToNot(HaveOccurred())
Expect(session.Email).To(Equal("upn@company.com"))
})
It("falls back to UPN claim on errors", func() {
p.oidcEnrichFunc = func(_ context.Context, s *sessions.SessionState) error {
return errors.New("neither the id_token nor the profileURL set an email")
}
err := p.EnrichSession(context.Background(), session)
Expect(err).ToNot(HaveOccurred())
Expect(session.Email).To(Equal("upn@company.com"))
})
})
Describe("RefreshSession", func() {
It("uses email claim if present", func() {
p.oidcRefreshFunc = func(_ context.Context, s *sessions.SessionState) (bool, error) {
s.Email = "person@company.com"
return true, nil
}
_, err := p.RefreshSession(context.Background(), session)
Expect(err).ToNot(HaveOccurred())
Expect(session.Email).To(Equal("person@company.com"))
})
It("falls back to UPN claim if Email is missing", func() {
p.oidcRefreshFunc = func(_ context.Context, s *sessions.SessionState) (bool, error) {
return true, nil
}
_, err := p.RefreshSession(context.Background(), session)
Expect(err).ToNot(HaveOccurred())
Expect(session.Email).To(Equal("upn@company.com"))
})
})
})
2021-06-13 10:00:12 +02:00
})