diff --git a/config.go b/config.go index c9c91b2..49e3361 100644 --- a/config.go +++ b/config.go @@ -79,8 +79,8 @@ func ExampleConfigDex() (c *Config) { c = DefaultConfig() c.ClientID = "example-app" c.ClientSecret = "ZXhhbXBsZS1hcHAtc2VjcmV0" - c.RedirectURL = "http://127.0.0.1:5555/callback" c.IssuerURL = "http://127.0.0.1:5556/dex" + c.RedirectURL = "http://127.0.0.1:5555/callback" return } @@ -90,8 +90,8 @@ func ExampleConfigGoogle() (c *Config) { c = DefaultConfig() c.ClientID = os.Getenv("GOOGLE_OAUTH2_CLIENT_ID") c.ClientSecret = os.Getenv("GOOGLE_OAUTH2_CLIENT_SECRET") - c.RedirectURL = "http://127.0.0.1:5556/auth/google/callback" c.IssuerURL = "https://accounts.google.com" + c.RedirectURL = "http://127.0.0.1:5556/auth/google/callback" return } @@ -99,22 +99,22 @@ func ExampleConfigGoogle() (c *Config) { func (c Config) Validate() (err error) { if c.ClientID == "" { - err = errors.New("ClientID Is required") + err = errors.New("ClientID is required") return } if c.ClientSecret == "" { - err = errors.New("ClientSecret Is required") + err = errors.New("ClientSecret is required") return } if c.IssuerURL == "" { // TODO: Validate that its a properly formed URL - err = errors.New("IssuerURL Is required") + err = errors.New("IssuerURL is required") return } if c.RedirectURL == "" { // TODO: Validate that its a properly formed URL - err = errors.New("RedirectURL Is required") + err = errors.New("RedirectURL is required") return } diff --git a/config_test.go b/config_test.go new file mode 100644 index 0000000..cf7f3b9 --- /dev/null +++ b/config_test.go @@ -0,0 +1,104 @@ +package oidcauth + +import ( + "os" + "testing" + + goblin "github.com/franela/goblin" + . "github.com/onsi/gomega" +) + +func TestConfig(t *testing.T) { + g := goblin.Goblin(t) + + //special hook for gomega + RegisterFailHandler(func(m string, _ ...int) { g.Fail(m) }) + + g.Describe("TestConfig", func() { + + g.Describe("DefaultConfig", func() { + os.Setenv("OIDC_CLIENT_ID", "client-id") + os.Setenv("OIDC_CLIENT_SECRET", "client-secret") + os.Setenv("OIDC_ISSUER_URL", "issuer-url") + os.Setenv("OIDC_REDIRECT_URL", "redirect-url") + c := DefaultConfig() + + g.It("should retrieve values from env", func() { + Expect(c.ClientID).To(BeEquivalentTo("client-id")) + Expect(c.ClientSecret).To(BeEquivalentTo("client-secret")) + Expect(c.IssuerURL).To(BeEquivalentTo("issuer-url")) + Expect(c.RedirectURL).To(BeEquivalentTo("redirect-url")) + }) + }) + + g.Describe("ExampleConfigDex", func() { + c := ExampleConfigDex() + + g.It("should match dex example-app config", func() { + Expect(c.ClientID).To(BeEquivalentTo("example-app")) + Expect(c.ClientSecret).To(BeEquivalentTo("ZXhhbXBsZS1hcHAtc2VjcmV0")) + Expect(c.IssuerURL).To(BeEquivalentTo("http://127.0.0.1:5556/dex")) + Expect(c.RedirectURL).To(BeEquivalentTo("http://127.0.0.1:5555/callback")) + }) + }) + + g.Describe("ExampleConfigGoogle", func() { + os.Setenv("GOOGLE_OAUTH2_CLIENT_ID", "client-id") + os.Setenv("GOOGLE_OAUTH2_CLIENT_SECRET", "client-secret") + c := ExampleConfigGoogle() + + g.It("should match example google config", func() { + Expect(c.ClientID).To(BeEquivalentTo("client-id")) + Expect(c.ClientSecret).To(BeEquivalentTo("client-secret")) + Expect(c.IssuerURL).To(BeEquivalentTo("https://accounts.google.com")) + Expect(c.RedirectURL).To(BeEquivalentTo("http://127.0.0.1:5556/auth/google/callback")) + }) + }) + + g.Describe("Validate", func() { + c := ExampleConfigDex() + c.ClientID = "" + g.It("should error on empty ClientID", func() { + Expect(c.Validate()).ToNot(BeNil()) + }) + + c = ExampleConfigDex() + c.ClientSecret = "" + g.It("should error on empty ClientSecret", func() { + Expect(c.Validate()).ToNot(BeNil()) + }) + + c = ExampleConfigDex() + c.IssuerURL = "" + g.It("should error on empty IssuerURL", func() { + Expect(c.Validate()).ToNot(BeNil()) + }) + + c = ExampleConfigDex() + c.RedirectURL = "" + g.It("should error on empty RedirectURL", func() { + Expect(c.Validate()).ToNot(BeNil()) + }) + }) + + // g.Describe("GetOidcAuth", func() { + // auth, err := GetOidcAuth(ExampleConfigDex()) + + // g.It("should work", func() { + // Expect(auth).NotTo(BeNil()) + // Expect(err).To(BeNil()) + // }) + // }) + + // g.Describe("c.GetOidcAuth", func() { + // c := ExampleConfigDex() + // auth, err := c.GetOidcAuth() + + // g.It("should work", func() { + // Expect(auth).NotTo(BeNil()) + // Expect(err).To(BeNil()) + // }) + // }) + + }) +} diff --git a/go.mod b/go.mod index c4d15f5..1eac088 100644 --- a/go.mod +++ b/go.mod @@ -4,10 +4,12 @@ go 1.15 require ( github.com/coreos/go-oidc/v3 v3.0.0 + github.com/franela/goblin v0.0.0-20210113153425-413781f5e6c8 github.com/gin-contrib/sessions v0.0.3 github.com/gin-gonic/gin v1.6.3 github.com/gorilla/sessions v1.2.1 // indirect github.com/letsencrypt/boulder v0.0.0-20210130012035-2a8f0fe6ac7e + github.com/onsi/gomega v1.5.0 github.com/sirupsen/logrus v1.4.2 golang.org/x/net v0.0.0-20210119194325-5f4716e94777 golang.org/x/oauth2 v0.0.0-20210126194326-f9ce19ea3013 diff --git a/go.sum b/go.sum index 5af02fc..64f6389 100644 --- a/go.sum +++ b/go.sum @@ -74,6 +74,8 @@ github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymF github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/franela/goblin v0.0.0-20210113153425-413781f5e6c8 h1:QVPknD9yAYAmmmERIxdPFY6yf8d7xqoieNs/1C9ieCk= +github.com/franela/goblin v0.0.0-20210113153425-413781f5e6c8/go.mod h1:VzmDKDJVZI3aJmnRI9VjAn9nJ8qPPsN1fqzr9dqInIo= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/getsentry/raven-go v0.0.0-20180121060056-563b81fc02b7/go.mod h1:KungGk8q33+aIAZUIVWZDr2OfAEBsO49PX4NzFV5kcQ= github.com/gin-contrib/sessions v0.0.3 h1:PoBXki+44XdJdlgDqDrY5nDVe3Wk7wDV/UCOuLP6fBI= @@ -232,6 +234,7 @@ github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRW github.com/nkovacs/streamquote v0.0.0-20170412213628-49af9bddb229/go.mod h1:0aYXnNPJ8l7uZxf45rWW1a/uME32OF0rhiYGNQ2oF2E= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.8.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/gomega v1.5.0 h1:izbySO9zDPmjJ8rDjLvkA2zJHIo+HkYXHnf7eN7SSyo= github.com/onsi/gomega v1.5.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= github.com/op/go-logging v0.0.0-20160315200505-970db520ece7/go.mod h1:HzydrMdWErDVzsI23lYNej1Htcns9BCg93Dk0bBINWk= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= diff --git a/oidc.go b/oidc.go index 89d915c..f7d962b 100644 --- a/oidc.go +++ b/oidc.go @@ -3,6 +3,7 @@ package oidcauth import ( "errors" "net/http" + "time" log "github.com/sirupsen/logrus" @@ -29,6 +30,9 @@ const ( // loginSessionKey is the session key to hold the "login" (username) loginSessionKey string = "oidcauth:login" + // expirationSessionKey is the when the session is expired (in unixtime as an uint) + expirationSessionKey string = "oidcauth:sessionExpiration" + // AuthUserKey stores the authenticated user's login (username or email) in this context key AuthUserKey string = "user" ) @@ -85,21 +89,29 @@ func newOidcAuth(c *Config) (o *OidcAuth, err error) { func (o *OidcAuth) AuthRequired() gin.HandlerFunc { return func(c *gin.Context) { session := sessions.Default(c) - token := session.Get(accessTokenSessionKey) - if token == nil { + e := session.Get(expirationSessionKey) + l := session.Get(loginSessionKey) + if l == nil || e == nil { o.doAuthentication(c) c.Abort() return } - // TODO: Valdate Token / Expiration? / Extension? - l := session.Get(loginSessionKey) - if l == nil { + + login := l.(string) + exp := time.Unix(int64(e.(float64)), 0) // e (float64) -> int64 -> unixtime -> time.Time + now := time.Now() + + if now.After(exp) { + log.WithFields(log.Fields{ + "login": login, + "exp": exp, + "now": now, + }).Info("Session Expired") o.doAuthentication(c) c.Abort() return } - login := l.(string) - // The user credentials was found, set user's id to key AuthUserKey in this context, the user's id can be read later using + // The user credentials was found, set user's loginClaim to key AuthUserKey in this context, the user's id can be read later using // c.MustGet(oidcauth.AuthUserKey). c.Set(AuthUserKey, login) c.Next() @@ -125,7 +137,7 @@ func (o *OidcAuth) Login(c *gin.Context) { func (o *OidcAuth) Logout(c *gin.Context) { session := sessions.Default(c) // These Sets will mark the session as "written" and clear the values (jic) - session.Set(accessTokenSessionKey, nil) + // session.Set(accessTokenSessionKey, nil) session.Set(loginSessionKey, nil) session.Clear() session.Options(sessions.Options{Path: "/", MaxAge: -1}) // this sets the cookie as expired @@ -183,7 +195,7 @@ func (o *OidcAuth) AuthCallback(c *gin.Context) { session.AddFlash("Authentication Successful!") // Process Results - just dump everything into the session for now (probably not a good idea) - session.Set(accessTokenSessionKey, oauth2Token.AccessToken) + // session.Set(accessTokenSessionKey, oauth2Token.AccessToken) // sessions doesn't like very long AccessToken // session.Set("TokenType", oauth2Token.TokenType) // Not Needed? // session.Set("Expiry", oauth2Token.Expiry) // sessions doesn't like time.Time delete(claims, "nonce") // No longer useful @@ -210,6 +222,11 @@ func (o *OidcAuth) AuthCallback(c *gin.Context) { session.Set(loginSessionKey, login) } + // Set expiration in session + if exp, ok := claims["exp"]; ok { + session.Set(expirationSessionKey, exp) + } + redirectURL := o.config.DefaultAuthenticatedURL u := session.Get(previousURLSessionKey) if u != nil {