@@ -0,0 +1,264 @@
1+package providers
2+
3+import (
4+ "context"
5+ "crypto/rand"
6+ "crypto/rsa"
7+ "encoding/base64"
8+ "encoding/json"
9+ "errors"
10+ "fmt"
11+ "golang.org/x/oauth2"
12+
13+ "github.com/bmizerany/assert"
14+ "github.com/coreos/go-oidc"
15+ "github.com/dgrijalva/jwt-go"
16+ "github.com/pusher/oauth2_proxy/pkg/apis/sessions"
17+
18+ "net/http"
19+ "net/http/httptest"
20+ "net/url"
21+ "strings"
22+ "testing"
23+ "time"
24+)
25+
26+const accessToken = "access_token"
27+const refreshToken = "refresh_token"
28+const clientID = "https://test.myapp.com"
29+const secret = "secret"
30+
31+type idTokenClaims struct {
32+ Name string `json:"name,omitempty"`
33+ Email string `json:"email,omitempty"`
34+ Picture string `json:"picture,omitempty"`
35+ jwt.StandardClaims
36+}
37+
38+type redeemTokenResponse struct {
39+ AccessToken string `json:"access_token"`
40+ RefreshToken string `json:"refresh_token"`
41+ ExpiresIn int64 `json:"expires_in"`
42+ TokenType string `json:"token_type"`
43+ IDToken string `json:"id_token,omitempty"`
44+}
45+
46+var defaultIDToken idTokenClaims = idTokenClaims{
47+ "Jane Dobbs",
48+ "janed@me.com",
49+ "http://mugbook.com/janed/me.jpg",
50+ jwt.StandardClaims{
51+ Audience: "https://test.myapp.com",
52+ ExpiresAt: time.Now().Add(time.Duration(5) * time.Minute).Unix(),
53+ Id: "id-some-id",
54+ IssuedAt: time.Now().Unix(),
55+ Issuer: "https://issuer.example.com",
56+ NotBefore: 0,
57+ Subject: "123456789",
58+ },
59+}
60+
61+type fakeKeySetStub struct {}
62+
63+func (fakeKeySetStub) VerifySignature(_ context.Context, jwt string) (payload []byte, err error) {
64+ decodeString, err := base64.RawURLEncoding.DecodeString(strings.Split(jwt, ".")[1])
65+ tokenClaims := &idTokenClaims{}
66+ err = json.Unmarshal(decodeString, tokenClaims)
67+
68+ if err != nil || tokenClaims.Id == "this-id-fails-validation" {
69+ return nil, fmt.Errorf("the validation failed for subject [%v]", tokenClaims.Subject)
70+ }
71+
72+ return decodeString, err
73+}
74+
75+func newOIDCProvider(serverURL *url.URL) *OIDCProvider {
76+
77+ providerData := &ProviderData{
78+ ProviderName: "oidc",
79+ ClientID: clientID,
80+ ClientSecret: secret,
81+ LoginURL: &url.URL{
82+ Scheme: serverURL.Scheme,
83+ Host: serverURL.Host,
84+ Path: "/login/oauth/authorize"},
85+ RedeemURL: &url.URL{
86+ Scheme: serverURL.Scheme,
87+ Host: serverURL.Host,
88+ Path: "/login/oauth/access_token"},
89+ ProfileURL: &url.URL{
90+ Scheme: serverURL.Scheme,
91+ Host: serverURL.Host,
92+ Path: "/profile"},
93+ ValidateURL: &url.URL{
94+ Scheme: serverURL.Scheme,
95+ Host: serverURL.Host,
96+ Path: "/api"},
97+ Scope: "openid profile offline_access"}
98+
99+ p := &OIDCProvider{
100+ ProviderData: providerData,
101+ Verifier: oidc.NewVerifier(
102+ "https://issuer.example.com",
103+ fakeKeySetStub{},
104+ &oidc.Config{ClientID: clientID},
105+ ),
106+ }
107+
108+ return p
109+}
110+
111+func newOIDCServer(body []byte) (*url.URL, *httptest.Server) {
112+ s := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
113+ rw.Header().Add("content-type", "application/json")
114+ _, _ = rw.Write(body)
115+ }))
116+ u, _ := url.Parse(s.URL)
117+ return u, s
118+}
119+
120+func newSignedTestIDToken(tokenClaims idTokenClaims) (string, error) {
121+
122+ key, _ := rsa.GenerateKey(rand.Reader, 2048)
123+ standardClaims := jwt.NewWithClaims(jwt.SigningMethodRS256, tokenClaims)
124+ return standardClaims.SignedString(key)
125+}
126+
127+func newOauth2Token() *oauth2.Token {
128+ return &oauth2.Token{
129+ AccessToken: accessToken,
130+ TokenType: "Bearer",
131+ RefreshToken: refreshToken,
132+ Expiry: time.Time{}.Add(time.Duration(5) * time.Second),
133+ }
134+}
135+
136+func newTestSetup(body []byte) (*httptest.Server, *OIDCProvider) {
137+ redeemURL, server := newOIDCServer(body)
138+ provider := newOIDCProvider(redeemURL)
139+ return server, provider
140+}
141+
142+func TestOIDCProviderRedeem(t *testing.T) {
143+
144+ idToken, _ := newSignedTestIDToken(defaultIDToken)
145+ body, _ := json.Marshal(redeemTokenResponse{
146+ AccessToken: accessToken,
147+ ExpiresIn: 10,
148+ TokenType: "Bearer",
149+ RefreshToken: refreshToken,
150+ IDToken: idToken,
151+ })
152+
153+ server, provider := newTestSetup(body)
154+ defer server.Close()
155+
156+ session, err := provider.Redeem(provider.RedeemURL.String(), "code1234")
157+ assert.Equal(t, nil, err)
158+ assert.Equal(t, defaultIDToken.Email, session.Email)
159+ assert.Equal(t, accessToken, session.AccessToken)
160+ assert.Equal(t, idToken, session.IDToken)
161+ assert.Equal(t, refreshToken, session.RefreshToken)
162+ assert.Equal(t, "123456789", session.User)
163+}
164+
165+func TestOIDCProviderRefreshSessionIfNeededWithoutIdToken(t *testing.T) {
166+
167+ idToken, _ := newSignedTestIDToken(defaultIDToken)
168+ body, _ := json.Marshal(redeemTokenResponse{
169+ AccessToken: accessToken,
170+ ExpiresIn: 10,
171+ TokenType: "Bearer",
172+ RefreshToken: refreshToken,
173+ })
174+
175+ server, provider := newTestSetup(body)
176+ defer server.Close()
177+
178+ existingSession := &sessions.SessionState{
179+ AccessToken: "changeit",
180+ IDToken: idToken,
181+ CreatedAt: time.Time{},
182+ ExpiresOn: time.Time{},
183+ RefreshToken: refreshToken,
184+ Email: "janedoe@example.com",
185+ User: "11223344",
186+ }
187+
188+ refreshed, err := provider.RefreshSessionIfNeeded(existingSession)
189+ assert.Equal(t, nil, err)
190+ assert.Equal(t, refreshed, true)
191+ assert.Equal(t, "janedoe@example.com", existingSession.Email)
192+ assert.Equal(t, accessToken, existingSession.AccessToken)
193+ assert.Equal(t, idToken, existingSession.IDToken)
194+ assert.Equal(t, refreshToken, existingSession.RefreshToken)
195+ assert.Equal(t, "11223344", existingSession.User)
196+}
197+
198+func TestOIDCProviderRefreshSessionIfNeededWithIdToken(t *testing.T) {
199+
200+ idToken, _ := newSignedTestIDToken(defaultIDToken)
201+ body, _ := json.Marshal(redeemTokenResponse{
202+ AccessToken: accessToken,
203+ ExpiresIn: 10,
204+ TokenType: "Bearer",
205+ RefreshToken: refreshToken,
206+ IDToken: idToken,
207+ })
208+
209+ server, provider := newTestSetup(body)
210+ defer server.Close()
211+
212+ existingSession := &sessions.SessionState{
213+ AccessToken: "changeit",
214+ IDToken: "changeit",
215+ CreatedAt: time.Time{},
216+ ExpiresOn: time.Time{},
217+ RefreshToken: refreshToken,
218+ Email: "changeit",
219+ User: "changeit",
220+ }
221+ refreshed, err := provider.RefreshSessionIfNeeded(existingSession)
222+ assert.Equal(t, nil, err)
223+ assert.Equal(t, refreshed, true)
224+ assert.Equal(t, defaultIDToken.Email, existingSession.Email)
225+ assert.Equal(t, defaultIDToken.Subject, existingSession.User)
226+ assert.Equal(t, accessToken, existingSession.AccessToken)
227+ assert.Equal(t, idToken, existingSession.IDToken)
228+ assert.Equal(t, refreshToken, existingSession.RefreshToken)
229+}
230+
231+func TestOIDCProvider_findVerifiedIdToken(t *testing.T) {
232+
233+ server, provider := newTestSetup([]byte(""))
234+
235+ defer server.Close()
236+
237+ token := newOauth2Token()
238+ signedIdToken, _ := newSignedTestIDToken(defaultIDToken)
239+ tokenWithIdToken := token.WithExtra(map[string]interface{}{
240+ "id_token": signedIdToken,
241+ })
242+
243+ verifiedIdToken, err := provider.findVerifiedIDToken(context.Background(), tokenWithIdToken)
244+ assert.Equal(t, true, err == nil)
245+ assert.Equal(t, true, verifiedIdToken != nil)
246+ assert.Equal(t, defaultIDToken.Issuer, verifiedIdToken.Issuer)
247+ assert.Equal(t, defaultIDToken.Subject, verifiedIdToken.Subject)
248+
249+ // When the validation fails the response should be nil
250+ defaultIDToken.Id = "this-id-fails-validation"
251+ signedIdToken, _ = newSignedTestIDToken(defaultIDToken)
252+ tokenWithIdToken = token.WithExtra(map[string]interface{}{
253+ "id_token": signedIdToken,
254+ })
255+
256+ verifiedIdToken, err = provider.findVerifiedIDToken(context.Background(), tokenWithIdToken)
257+ assert.Equal(t, errors.New("failed to verify signature: the validation failed for subject [123456789]"), err)
258+ assert.Equal(t, true, verifiedIdToken == nil)
259+
260+ // When there is no id token in the oauth token
261+ verifiedIdToken, err = provider.findVerifiedIDToken(context.Background(), newOauth2Token())
262+ assert.Equal(t, nil, err)
263+ assert.Equal(t, true, verifiedIdToken == nil)
264+}