Merge remote-tracking branch 'upstream/master' into helm-example
# Conflicts: # CHANGELOG.md
This commit is contained in:
		
						commit
						054979978f
					
				| 
						 | 
					@ -56,6 +56,7 @@
 | 
				
			||||||
## Changes since v5.1.1
 | 
					## Changes since v5.1.1
 | 
				
			||||||
 | 
					
 | 
				
			||||||
- [#615](https://github.com/oauth2-proxy/oauth2-proxy/pull/615) Kubernetes example based on Kind cluster and Nginx ingress (@EvgeniGordeev)
 | 
					- [#615](https://github.com/oauth2-proxy/oauth2-proxy/pull/615) Kubernetes example based on Kind cluster and Nginx ingress (@EvgeniGordeev)
 | 
				
			||||||
 | 
					- [#596](https://github.com/oauth2-proxy/oauth2-proxy/pull/596) Validate Bearer IDTokens in headers with correct provider/extra JWT Verifier (@NickMeves)
 | 
				
			||||||
- [#620](https://github.com/oauth2-proxy/oauth2-proxy/pull/620) Add HealthCheck middleware (@JoelSpeed)
 | 
					- [#620](https://github.com/oauth2-proxy/oauth2-proxy/pull/620) Add HealthCheck middleware (@JoelSpeed)
 | 
				
			||||||
- [#597](https://github.com/oauth2-proxy/oauth2-proxy/pull/597) Don't log invalid redirect if redirect is empty (@JoelSpeed)
 | 
					- [#597](https://github.com/oauth2-proxy/oauth2-proxy/pull/597) Don't log invalid redirect if redirect is empty (@JoelSpeed)
 | 
				
			||||||
- [#604](https://github.com/oauth2-proxy/oauth2-proxy/pull/604) Add Keycloak local testing environment (@EvgeniGordeev)
 | 
					- [#604](https://github.com/oauth2-proxy/oauth2-proxy/pull/604) Add Keycloak local testing environment (@EvgeniGordeev)
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										129
									
								
								oauthproxy.go
								
								
								
								
							
							
						
						
									
										129
									
								
								oauthproxy.go
								
								
								
								
							| 
						 | 
					@ -88,35 +88,36 @@ type OAuthProxy struct {
 | 
				
			||||||
	AuthOnlyPath      string
 | 
						AuthOnlyPath      string
 | 
				
			||||||
	UserInfoPath      string
 | 
						UserInfoPath      string
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	redirectURL          *url.URL // the url to receive requests at
 | 
						redirectURL             *url.URL // the url to receive requests at
 | 
				
			||||||
	whitelistDomains     []string
 | 
						whitelistDomains        []string
 | 
				
			||||||
	provider             providers.Provider
 | 
						provider                providers.Provider
 | 
				
			||||||
	providerNameOverride string
 | 
						providerNameOverride    string
 | 
				
			||||||
	sessionStore         sessionsapi.SessionStore
 | 
						sessionStore            sessionsapi.SessionStore
 | 
				
			||||||
	ProxyPrefix          string
 | 
						ProxyPrefix             string
 | 
				
			||||||
	SignInMessage        string
 | 
						SignInMessage           string
 | 
				
			||||||
	HtpasswdFile         *HtpasswdFile
 | 
						HtpasswdFile            *HtpasswdFile
 | 
				
			||||||
	DisplayHtpasswdForm  bool
 | 
						DisplayHtpasswdForm     bool
 | 
				
			||||||
	serveMux             http.Handler
 | 
						serveMux                http.Handler
 | 
				
			||||||
	SetXAuthRequest      bool
 | 
						SetXAuthRequest         bool
 | 
				
			||||||
	PassBasicAuth        bool
 | 
						PassBasicAuth           bool
 | 
				
			||||||
	SetBasicAuth         bool
 | 
						SetBasicAuth            bool
 | 
				
			||||||
	SkipProviderButton   bool
 | 
						SkipProviderButton      bool
 | 
				
			||||||
	PassUserHeaders      bool
 | 
						PassUserHeaders         bool
 | 
				
			||||||
	BasicAuthPassword    string
 | 
						BasicAuthPassword       string
 | 
				
			||||||
	PassAccessToken      bool
 | 
						PassAccessToken         bool
 | 
				
			||||||
	SetAuthorization     bool
 | 
						SetAuthorization        bool
 | 
				
			||||||
	PassAuthorization    bool
 | 
						PassAuthorization       bool
 | 
				
			||||||
	PreferEmailToUser    bool
 | 
						PreferEmailToUser       bool
 | 
				
			||||||
	skipAuthRegex        []string
 | 
						skipAuthRegex           []string
 | 
				
			||||||
	skipAuthPreflight    bool
 | 
						skipAuthPreflight       bool
 | 
				
			||||||
	skipJwtBearerTokens  bool
 | 
						skipJwtBearerTokens     bool
 | 
				
			||||||
	jwtBearerVerifiers   []*oidc.IDTokenVerifier
 | 
						mainJwtBearerVerifier   *oidc.IDTokenVerifier
 | 
				
			||||||
	compiledRegex        []*regexp.Regexp
 | 
						extraJwtBearerVerifiers []*oidc.IDTokenVerifier
 | 
				
			||||||
	templates            *template.Template
 | 
						compiledRegex           []*regexp.Regexp
 | 
				
			||||||
	realClientIPParser   ipapi.RealClientIPParser
 | 
						templates               *template.Template
 | 
				
			||||||
	Banner               string
 | 
						realClientIPParser      ipapi.RealClientIPParser
 | 
				
			||||||
	Footer               string
 | 
						Banner                  string
 | 
				
			||||||
 | 
						Footer                  string
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// UpstreamProxy represents an upstream server to proxy to
 | 
					// UpstreamProxy represents an upstream server to proxy to
 | 
				
			||||||
| 
						 | 
					@ -317,32 +318,33 @@ func NewOAuthProxy(opts *options.Options, validator func(string) bool) *OAuthPro
 | 
				
			||||||
		AuthOnlyPath:      fmt.Sprintf("%s/auth", opts.ProxyPrefix),
 | 
							AuthOnlyPath:      fmt.Sprintf("%s/auth", opts.ProxyPrefix),
 | 
				
			||||||
		UserInfoPath:      fmt.Sprintf("%s/userinfo", opts.ProxyPrefix),
 | 
							UserInfoPath:      fmt.Sprintf("%s/userinfo", opts.ProxyPrefix),
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		ProxyPrefix:          opts.ProxyPrefix,
 | 
							ProxyPrefix:             opts.ProxyPrefix,
 | 
				
			||||||
		provider:             opts.GetProvider(),
 | 
							provider:                opts.GetProvider(),
 | 
				
			||||||
		providerNameOverride: opts.ProviderName,
 | 
							providerNameOverride:    opts.ProviderName,
 | 
				
			||||||
		sessionStore:         opts.GetSessionStore(),
 | 
							sessionStore:            opts.GetSessionStore(),
 | 
				
			||||||
		serveMux:             serveMux,
 | 
							serveMux:                serveMux,
 | 
				
			||||||
		redirectURL:          redirectURL,
 | 
							redirectURL:             redirectURL,
 | 
				
			||||||
		whitelistDomains:     opts.WhitelistDomains,
 | 
							whitelistDomains:        opts.WhitelistDomains,
 | 
				
			||||||
		skipAuthRegex:        opts.SkipAuthRegex,
 | 
							skipAuthRegex:           opts.SkipAuthRegex,
 | 
				
			||||||
		skipAuthPreflight:    opts.SkipAuthPreflight,
 | 
							skipAuthPreflight:       opts.SkipAuthPreflight,
 | 
				
			||||||
		skipJwtBearerTokens:  opts.SkipJwtBearerTokens,
 | 
							skipJwtBearerTokens:     opts.SkipJwtBearerTokens,
 | 
				
			||||||
		jwtBearerVerifiers:   opts.GetJWTBearerVerifiers(),
 | 
							mainJwtBearerVerifier:   opts.GetOIDCVerifier(),
 | 
				
			||||||
		compiledRegex:        opts.GetCompiledRegex(),
 | 
							extraJwtBearerVerifiers: opts.GetJWTBearerVerifiers(),
 | 
				
			||||||
		realClientIPParser:   opts.GetRealClientIPParser(),
 | 
							compiledRegex:           opts.GetCompiledRegex(),
 | 
				
			||||||
		SetXAuthRequest:      opts.SetXAuthRequest,
 | 
							realClientIPParser:      opts.GetRealClientIPParser(),
 | 
				
			||||||
		PassBasicAuth:        opts.PassBasicAuth,
 | 
							SetXAuthRequest:         opts.SetXAuthRequest,
 | 
				
			||||||
		SetBasicAuth:         opts.SetBasicAuth,
 | 
							PassBasicAuth:           opts.PassBasicAuth,
 | 
				
			||||||
		PassUserHeaders:      opts.PassUserHeaders,
 | 
							SetBasicAuth:            opts.SetBasicAuth,
 | 
				
			||||||
		BasicAuthPassword:    opts.BasicAuthPassword,
 | 
							PassUserHeaders:         opts.PassUserHeaders,
 | 
				
			||||||
		PassAccessToken:      opts.PassAccessToken,
 | 
							BasicAuthPassword:       opts.BasicAuthPassword,
 | 
				
			||||||
		SetAuthorization:     opts.SetAuthorization,
 | 
							PassAccessToken:         opts.PassAccessToken,
 | 
				
			||||||
		PassAuthorization:    opts.PassAuthorization,
 | 
							SetAuthorization:        opts.SetAuthorization,
 | 
				
			||||||
		PreferEmailToUser:    opts.PreferEmailToUser,
 | 
							PassAuthorization:       opts.PassAuthorization,
 | 
				
			||||||
		SkipProviderButton:   opts.SkipProviderButton,
 | 
							PreferEmailToUser:       opts.PreferEmailToUser,
 | 
				
			||||||
		templates:            loadTemplates(opts.CustomTemplatesDir),
 | 
							SkipProviderButton:      opts.SkipProviderButton,
 | 
				
			||||||
		Banner:               opts.Banner,
 | 
							templates:               loadTemplates(opts.CustomTemplatesDir),
 | 
				
			||||||
		Footer:               opts.Footer,
 | 
							Banner:                  opts.Banner,
 | 
				
			||||||
 | 
							Footer:                  opts.Footer,
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1139,15 +1141,24 @@ func (p *OAuthProxy) GetJwtSession(req *http.Request) (*sessionsapi.SessionState
 | 
				
			||||||
		return nil, err
 | 
							return nil, err
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	for _, verifier := range p.jwtBearerVerifiers {
 | 
						// If we are using an oidc provider, go ahead and try that provider first with its Verifier
 | 
				
			||||||
		bearerToken, err := verifier.Verify(req.Context(), rawBearerToken)
 | 
						// and Bearer Token -> Session converter
 | 
				
			||||||
 | 
						if p.mainJwtBearerVerifier != nil {
 | 
				
			||||||
 | 
							bearerToken, err := p.mainJwtBearerVerifier.Verify(req.Context(), rawBearerToken)
 | 
				
			||||||
 | 
							if err == nil {
 | 
				
			||||||
 | 
								return p.provider.CreateSessionStateFromBearerToken(req.Context(), rawBearerToken, bearerToken)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Otherwise, attempt to verify against the extra JWT issuers and use a more generic
 | 
				
			||||||
 | 
						// Bearer Token -> Session converter
 | 
				
			||||||
 | 
						for _, verifier := range p.extraJwtBearerVerifiers {
 | 
				
			||||||
 | 
							bearerToken, err := verifier.Verify(req.Context(), rawBearerToken)
 | 
				
			||||||
		if err != nil {
 | 
							if err != nil {
 | 
				
			||||||
			logger.Printf("failed to verify bearer token: %v", err)
 | 
					 | 
				
			||||||
			continue
 | 
								continue
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		return p.provider.CreateSessionStateFromBearerToken(req.Context(), rawBearerToken, bearerToken)
 | 
							return (*providers.ProviderData)(nil).CreateSessionStateFromBearerToken(req.Context(), rawBearerToken, bearerToken)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	return nil, fmt.Errorf("unable to verify jwt token %s", req.Header.Get("Authorization"))
 | 
						return nil, fmt.Errorf("unable to verify jwt token %s", req.Header.Get("Authorization"))
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1578,7 +1578,7 @@ func TestGetJwtSession(t *testing.T) {
 | 
				
			||||||
	// Bearer
 | 
						// Bearer
 | 
				
			||||||
	expires := time.Unix(1912151821, 0)
 | 
						expires := time.Unix(1912151821, 0)
 | 
				
			||||||
	session, _ := test.proxy.GetJwtSession(test.req)
 | 
						session, _ := test.proxy.GetJwtSession(test.req)
 | 
				
			||||||
	assert.Equal(t, session.User, "john@example.com")
 | 
						assert.Equal(t, session.User, "1234567890")
 | 
				
			||||||
	assert.Equal(t, session.Email, "john@example.com")
 | 
						assert.Equal(t, session.Email, "john@example.com")
 | 
				
			||||||
	assert.Equal(t, session.ExpiresOn, &expires)
 | 
						assert.Equal(t, session.ExpiresOn, &expires)
 | 
				
			||||||
	assert.Equal(t, session.IDToken, goodJwt)
 | 
						assert.Equal(t, session.IDToken, goodJwt)
 | 
				
			||||||
| 
						 | 
					@ -1590,12 +1590,12 @@ func TestGetJwtSession(t *testing.T) {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// Check PassAuthorization, should overwrite Basic header
 | 
						// Check PassAuthorization, should overwrite Basic header
 | 
				
			||||||
	assert.Equal(t, test.req.Header.Get("Authorization"), authHeader)
 | 
						assert.Equal(t, test.req.Header.Get("Authorization"), authHeader)
 | 
				
			||||||
	assert.Equal(t, test.req.Header.Get("X-Forwarded-User"), "john@example.com")
 | 
						assert.Equal(t, test.req.Header.Get("X-Forwarded-User"), "1234567890")
 | 
				
			||||||
	assert.Equal(t, test.req.Header.Get("X-Forwarded-Email"), "john@example.com")
 | 
						assert.Equal(t, test.req.Header.Get("X-Forwarded-Email"), "john@example.com")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// SetAuthorization and SetXAuthRequest
 | 
						// SetAuthorization and SetXAuthRequest
 | 
				
			||||||
	assert.Equal(t, test.rw.Header().Get("Authorization"), authHeader)
 | 
						assert.Equal(t, test.rw.Header().Get("Authorization"), authHeader)
 | 
				
			||||||
	assert.Equal(t, test.rw.Header().Get("X-Auth-Request-User"), "john@example.com")
 | 
						assert.Equal(t, test.rw.Header().Get("X-Auth-Request-User"), "1234567890")
 | 
				
			||||||
	assert.Equal(t, test.rw.Header().Get("X-Auth-Request-Email"), "john@example.com")
 | 
						assert.Equal(t, test.rw.Header().Get("X-Auth-Request-Email"), "john@example.com")
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -178,10 +178,6 @@ func Validate(o *options.Options) error {
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if o.SkipJwtBearerTokens {
 | 
						if o.SkipJwtBearerTokens {
 | 
				
			||||||
		// If we are using an oidc provider, go ahead and add that provider to the list
 | 
					 | 
				
			||||||
		if o.GetOIDCVerifier() != nil {
 | 
					 | 
				
			||||||
			o.SetJWTBearerVerifiers(append(o.GetJWTBearerVerifiers(), o.GetOIDCVerifier()))
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
		// Configure extra issuers
 | 
							// Configure extra issuers
 | 
				
			||||||
		if len(o.ExtraJwtIssuers) > 0 {
 | 
							if len(o.ExtraJwtIssuers) > 0 {
 | 
				
			||||||
			var jwtIssuers []jwtIssuer
 | 
								var jwtIssuers []jwtIssuer
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -124,7 +124,6 @@ func newOIDCServer(body []byte) (*url.URL, *httptest.Server) {
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func newSignedTestIDToken(tokenClaims idTokenClaims) (string, error) {
 | 
					func newSignedTestIDToken(tokenClaims idTokenClaims) (string, error) {
 | 
				
			||||||
 | 
					 | 
				
			||||||
	key, _ := rsa.GenerateKey(rand.Reader, 2048)
 | 
						key, _ := rsa.GenerateKey(rand.Reader, 2048)
 | 
				
			||||||
	standardClaims := jwt.NewWithClaims(jwt.SigningMethodRS256, tokenClaims)
 | 
						standardClaims := jwt.NewWithClaims(jwt.SigningMethodRS256, tokenClaims)
 | 
				
			||||||
	return standardClaims.SignedString(key)
 | 
						return standardClaims.SignedString(key)
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -164,14 +164,13 @@ func (p *ProviderData) CreateSessionStateFromBearerToken(ctx context.Context, ra
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	newSession := &sessions.SessionState{
 | 
						newSession := &sessions.SessionState{
 | 
				
			||||||
		Email:             claims.Email,
 | 
							Email:             claims.Email,
 | 
				
			||||||
		User:              claims.Email,
 | 
							User:              claims.Subject,
 | 
				
			||||||
		PreferredUsername: claims.PreferredUsername,
 | 
							PreferredUsername: claims.PreferredUsername,
 | 
				
			||||||
 | 
							AccessToken:       rawIDToken,
 | 
				
			||||||
 | 
							IDToken:           rawIDToken,
 | 
				
			||||||
 | 
							RefreshToken:      "",
 | 
				
			||||||
 | 
							ExpiresOn:         &idToken.Expiry,
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	newSession.AccessToken = rawIDToken
 | 
					 | 
				
			||||||
	newSession.IDToken = rawIDToken
 | 
					 | 
				
			||||||
	newSession.RefreshToken = ""
 | 
					 | 
				
			||||||
	newSession.ExpiresOn = &idToken.Expiry
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	return newSession, nil
 | 
						return newSession, nil
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -2,10 +2,15 @@ package providers
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import (
 | 
					import (
 | 
				
			||||||
	"context"
 | 
						"context"
 | 
				
			||||||
 | 
						"crypto/rand"
 | 
				
			||||||
 | 
						"crypto/rsa"
 | 
				
			||||||
	"net/url"
 | 
						"net/url"
 | 
				
			||||||
	"testing"
 | 
						"testing"
 | 
				
			||||||
	"time"
 | 
						"time"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"github.com/coreos/go-oidc"
 | 
				
			||||||
 | 
						"github.com/dgrijalva/jwt-go"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	"github.com/oauth2-proxy/oauth2-proxy/pkg/apis/sessions"
 | 
						"github.com/oauth2-proxy/oauth2-proxy/pkg/apis/sessions"
 | 
				
			||||||
	"github.com/stretchr/testify/assert"
 | 
						"github.com/stretchr/testify/assert"
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
| 
						 | 
					@ -47,3 +52,39 @@ func TestAcrValuesConfigured(t *testing.T) {
 | 
				
			||||||
	result := p.GetLoginURL("https://my.test.app/oauth", "")
 | 
						result := p.GetLoginURL("https://my.test.app/oauth", "")
 | 
				
			||||||
	assert.Contains(t, result, "acr_values=testValue")
 | 
						assert.Contains(t, result, "acr_values=testValue")
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func TestCreateSessionStateFromBearerToken(t *testing.T) {
 | 
				
			||||||
 | 
						minimalIDToken := jwt.StandardClaims{
 | 
				
			||||||
 | 
							Audience:  "asdf1234",
 | 
				
			||||||
 | 
							ExpiresAt: time.Now().Add(time.Duration(5) * time.Minute).Unix(),
 | 
				
			||||||
 | 
							Id:        "id-some-id",
 | 
				
			||||||
 | 
							IssuedAt:  time.Now().Unix(),
 | 
				
			||||||
 | 
							Issuer:    "https://issuer.example.com",
 | 
				
			||||||
 | 
							NotBefore: 0,
 | 
				
			||||||
 | 
							Subject:   "123456789",
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						// From oidc_test.go
 | 
				
			||||||
 | 
						verifier := oidc.NewVerifier(
 | 
				
			||||||
 | 
							"https://issuer.example.com",
 | 
				
			||||||
 | 
							fakeKeySetStub{},
 | 
				
			||||||
 | 
							&oidc.Config{ClientID: "asdf1234"},
 | 
				
			||||||
 | 
						)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						key, err := rsa.GenerateKey(rand.Reader, 2048)
 | 
				
			||||||
 | 
						assert.NoError(t, err)
 | 
				
			||||||
 | 
						rawIDToken, err := jwt.NewWithClaims(jwt.SigningMethodRS256, minimalIDToken).SignedString(key)
 | 
				
			||||||
 | 
						assert.NoError(t, err)
 | 
				
			||||||
 | 
						// Pass to a dummy Verifier to get an oidc.IDToken from the rawIDToken for our actual test below
 | 
				
			||||||
 | 
						idToken, err := verifier.Verify(context.Background(), rawIDToken)
 | 
				
			||||||
 | 
						assert.NoError(t, err)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						session, err := (*ProviderData)(nil).CreateSessionStateFromBearerToken(context.Background(), rawIDToken, idToken)
 | 
				
			||||||
 | 
						assert.NoError(t, err)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						assert.Equal(t, rawIDToken, session.AccessToken)
 | 
				
			||||||
 | 
						assert.Equal(t, rawIDToken, session.IDToken)
 | 
				
			||||||
 | 
						assert.Equal(t, "123456789", session.Email)
 | 
				
			||||||
 | 
						assert.Equal(t, "123456789", session.User)
 | 
				
			||||||
 | 
						assert.Empty(t, session.RefreshToken)
 | 
				
			||||||
 | 
						assert.Empty(t, session.PreferredUsername)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
		Loading…
	
		Reference in New Issue