Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Logout with context token #194

Merged
merged 5 commits into from Jan 4, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
3 changes: 2 additions & 1 deletion docs/auth.md
Expand Up @@ -141,8 +141,9 @@ query-string, to protect against CSRF attack on this (this can part of bigger
attacks like session fixation).

```http
DELETE /auth/login?CtxToken=token-for-a-private-context HTTP/1.1
DELETE /auth/login HTTP/1.1
Host: cozy.example.org
Authorization: Bearer token-for-a-private-context
```

### POST /auth/register
Expand Down
25 changes: 25 additions & 0 deletions pkg/apps/apps.go
Expand Up @@ -6,7 +6,11 @@ import (

"github.com/cozy/cozy-stack/pkg/consts"
"github.com/cozy/cozy-stack/pkg/couchdb"
"github.com/cozy/cozy-stack/pkg/crypto"
"github.com/cozy/cozy-stack/pkg/instance"
"github.com/cozy/cozy-stack/pkg/permissions"
"github.com/cozy/cozy-stack/web/jsonapi"
jwt "gopkg.in/dgrijalva/jwt-go.v3"
)

const (
Expand Down Expand Up @@ -184,3 +188,24 @@ func contextMatches(path, ctx []string) bool {
}
return true
}

// BuildCtxToken is used to build a context token to identify the app for
// requests made to the stack
func (m *Manifest) BuildCtxToken(i *instance.Instance, ctx Context) string {
if ctx.Public {
return ""
}
token, err := crypto.NewJWT(i.SessionSecret, permissions.Claims{
StandardClaims: jwt.StandardClaims{
Audience: permissions.ContextAudience,
Issuer: i.Domain,
IssuedAt: crypto.Timestamp(),
Subject: m.Slug,
},
Scope: "", // TODO scope
})
if err != nil {
return ""
}
return token
}
47 changes: 47 additions & 0 deletions pkg/apps/apps_test.go
Expand Up @@ -3,6 +3,9 @@ package apps
import (
"testing"

"github.com/cozy/cozy-stack/pkg/crypto"
"github.com/cozy/cozy-stack/pkg/instance"
jwt "github.com/dgrijalva/jwt-go"
"github.com/stretchr/testify/assert"
)

Expand Down Expand Up @@ -59,3 +62,47 @@ func TestFindContext(t *testing.T) {
ctx, _ = manifest.FindContext("/")
assert.Equal(t, "", ctx.Folder)
}

func TestBuildCtxTokenReturnsBlankForPublicContext(t *testing.T) {
manifest := &Manifest{
Slug: "my-app",
}
manifest.Contexts = make(Contexts)
manifest.Contexts["/public"] = Context{Folder: "/public", Index: "index.html", Public: true}
ctx := manifest.Contexts["/public"]
i := &instance.Instance{
Domain: "test-ctx-token.example.com",
SessionSecret: crypto.GenerateRandomBytes(64),
}

tokenString := manifest.BuildCtxToken(i, ctx)
assert.Equal(t, "", tokenString)
}

func TestBuildCtxToken(t *testing.T) {
manifest := &Manifest{
Slug: "my-app",
}
manifest.Contexts = make(Contexts)
manifest.Contexts["/foo"] = Context{Folder: "/foo", Index: "index.html", Public: false}
ctx := manifest.Contexts["/foo"]
i := &instance.Instance{
Domain: "test-ctx-token.example.com",
SessionSecret: crypto.GenerateRandomBytes(64),
}

tokenString := manifest.BuildCtxToken(i, ctx)
token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
_, ok := token.Method.(*jwt.SigningMethodHMAC)
assert.True(t, ok, "The signing method should be HMAC")
return i.SessionSecret, nil
})
assert.NoError(t, err)
assert.True(t, token.Valid)

claims, ok := token.Claims.(jwt.MapClaims)
assert.True(t, ok, "Claims can be parsed as standard claims")
assert.Equal(t, "context", claims["aud"])
assert.Equal(t, "test-ctx-token.example.com", claims["iss"])
assert.Equal(t, "my-app", claims["sub"])
}
4 changes: 2 additions & 2 deletions pkg/crypto/jwt.go
Expand Up @@ -19,12 +19,12 @@ func NewJWT(secret []byte, claims jwt.Claims) (string, error) {
}

// ParseJWT parses a string and checkes that is a valid JSON Web Token
func ParseJWT(tokenString string, secret []byte, claims jwt.Claims) error {
func ParseJWT(tokenString string, keyFunc jwt.Keyfunc, claims jwt.Claims) error {
token, err := jwt.ParseWithClaims(tokenString, claims, func(token *jwt.Token) (interface{}, error) {
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, fmt.Errorf("Unexpected signing method: %v", token.Header["alg"])
}
return secret, nil
return keyFunc(token)
})

if err != nil {
Expand Down
12 changes: 9 additions & 3 deletions pkg/crypto/jwt_test.go
Expand Up @@ -51,7 +51,9 @@ func TestParseJWT(t *testing.T) {
assert.NoError(t, err)

claims := Claims{}
err = ParseJWT(tokenString, secret, &claims)
err = ParseJWT(tokenString, func(token *jwt.Token) (interface{}, error) {
return secret, nil
}, &claims)
assert.NoError(t, err)
assert.Equal(t, "test", claims.Audience)
assert.Equal(t, "example.org", claims.Issuer)
Expand All @@ -72,10 +74,14 @@ func TestParseInvalidJWT(t *testing.T) {
})
assert.NoError(t, err)

err = ParseJWT("invalid-token", secret, &Claims{})
err = ParseJWT("invalid-token", func(token *jwt.Token) (interface{}, error) {
return secret, nil
}, &Claims{})
assert.Error(t, err)

invalidSecret := GenerateRandomBytes(64)
err = ParseJWT(tokenString, invalidSecret, &Claims{})
err = ParseJWT(tokenString, func(token *jwt.Token) (interface{}, error) {
return invalidSecret, nil
}, &Claims{})
assert.Error(t, err)
}
2 changes: 1 addition & 1 deletion web/auth/access_code.go → pkg/oauth/access_code.go
@@ -1,4 +1,4 @@
package auth
package oauth

import (
"github.com/cozy/cozy-stack/pkg/consts"
Expand Down
49 changes: 19 additions & 30 deletions web/auth/client.go → pkg/oauth/client.go
@@ -1,4 +1,4 @@
package auth
package oauth

import (
"fmt"
Expand All @@ -11,22 +11,12 @@ import (
"github.com/cozy/cozy-stack/pkg/couchdb"
"github.com/cozy/cozy-stack/pkg/crypto"
"github.com/cozy/cozy-stack/pkg/instance"
"github.com/cozy/cozy-stack/pkg/permissions"
jwt "gopkg.in/dgrijalva/jwt-go.v3"
)

const (
// ClientSecretLen is the number of random bytes used for generating the client secret
ClientSecretLen = 24

// RegistrationTokenAudience is the audience field of JWT for registration tokens
RegistrationTokenAudience = "registration"

// AccessTokenAudience is the audience field of JWT for access tokens
AccessTokenAudience = "access"

// RefreshTokenAudience is the audience field of JWT for refresh tokens
RefreshTokenAudience = "refresh"
)
// ClientSecretLen is the number of random bytes used for generating the client secret
const ClientSecretLen = 24

// Client is a struct for OAuth2 client. Most of the fields are described in
// the OAuth 2.0 Dynamic Client Registration Protocol. The exception is
Expand Down Expand Up @@ -72,7 +62,9 @@ func (c *Client) SetID(id string) { c.CouchID = id }
// SetRev changes the client revision
func (c *Client) SetRev(rev string) { c.CouchRev = rev }

func (c *Client) transformIDAndRev() {
// TransformIDAndRev makes the translation from the JSON of CouchDB to the
// one used in the dynamic client registration protocol
func (c *Client) TransformIDAndRev() {
c.ClientID = c.CouchID
c.CouchID = ""
c.CouchRev = ""
Expand Down Expand Up @@ -160,7 +152,7 @@ func (c *Client) Create(i *instance.Instance) *ClientRegistrationError {

var err error
c.RegistrationToken, err = crypto.NewJWT(i.OAuthSecret, jwt.StandardClaims{
Audience: RegistrationTokenAudience,
Audience: permissions.RegistrationTokenAudience,
Issuer: i.Domain,
IssuedAt: time.Now().Unix(),
Subject: c.CouchID,
Expand All @@ -173,7 +165,7 @@ func (c *Client) Create(i *instance.Instance) *ClientRegistrationError {
}
}

c.transformIDAndRev()
c.TransformIDAndRev()
return nil
}

Expand Down Expand Up @@ -220,7 +212,7 @@ func (c *Client) Update(i *instance.Instance, old *Client) *ClientRegistrationEr
}
}

c.transformIDAndRev()
c.TransformIDAndRev()
return nil
}

Expand All @@ -246,22 +238,16 @@ func (c *Client) AcceptRedirectURI(u string) bool {
return false
}

// Claims is used for JWT used in OAuth2 flow
type Claims struct {
jwt.StandardClaims
Scope string `json:"scope,omitempty"`
}

// CreateJWT returns a new JSON Web Token for the given instance and audience
func (c *Client) CreateJWT(i *instance.Instance, audience, scope string) (string, error) {
token, err := crypto.NewJWT(i.OAuthSecret, Claims{
jwt.StandardClaims{
token, err := crypto.NewJWT(i.OAuthSecret, permissions.Claims{
StandardClaims: jwt.StandardClaims{
Audience: audience,
Issuer: i.Domain,
IssuedAt: crypto.Timestamp(),
Subject: c.CouchID,
},
scope,
Scope: scope,
})
if err != nil {
log.Errorf("[oauth] Failed to create the %s token: %s", audience, err)
Expand All @@ -270,12 +256,15 @@ func (c *Client) CreateJWT(i *instance.Instance, audience, scope string) (string
}

// ValidToken checks that the JWT is valid and returns the associate claims
func (c *Client) ValidToken(i *instance.Instance, audience, token string) (Claims, bool) {
claims := Claims{}
func (c *Client) ValidToken(i *instance.Instance, audience, token string) (permissions.Claims, bool) {
claims := permissions.Claims{}
if token == "" {
return claims, false
}
if err := crypto.ParseJWT(token, i.OAuthSecret, &claims); err != nil {
keyFunc := func(token *jwt.Token) (interface{}, error) {
return i.OAuthSecret, nil
}
if err := crypto.ParseJWT(token, keyFunc, &claims); err != nil {
log.Errorf("[oauth] Failed to verify the %s token: %s", audience, err)
return claims, false
}
Expand Down
13 changes: 7 additions & 6 deletions web/auth/client_test.go → pkg/oauth/client_test.go
@@ -1,12 +1,13 @@
package auth
package oauth

import (
"testing"

"github.com/cozy/cozy-stack/pkg/crypto"
"github.com/cozy/cozy-stack/pkg/instance"
jwt "github.com/dgrijalva/jwt-go"
"github.com/cozy/cozy-stack/pkg/permissions"
"github.com/stretchr/testify/assert"
jwt "gopkg.in/dgrijalva/jwt-go.v3"
)

var instanceSecret = crypto.GenerateRandomBytes(64)
Expand Down Expand Up @@ -42,7 +43,7 @@ func TestParseJWT(t *testing.T) {
tokenString, err := c.CreateJWT(in, "refresh", "foo:read")
assert.NoError(t, err)

claims, ok := c.ValidToken(in, RefreshTokenAudience, tokenString)
claims, ok := c.ValidToken(in, permissions.RefreshTokenAudience, tokenString)
assert.True(t, ok, "The token must be valid")
assert.Equal(t, "refresh", claims.Audience)
assert.Equal(t, "test-jwt.example.org", claims.Issuer)
Expand All @@ -53,7 +54,7 @@ func TestParseJWT(t *testing.T) {
func TestParseJWTInvalidAudience(t *testing.T) {
tokenString, err := c.CreateJWT(in, "access", "foo:read")
assert.NoError(t, err)
_, ok := c.ValidToken(in, RefreshTokenAudience, tokenString)
_, ok := c.ValidToken(in, permissions.RefreshTokenAudience, tokenString)
assert.False(t, ok, "The token should be invalid")
}

Expand All @@ -64,7 +65,7 @@ func TestParseJWTInvalidIssuer(t *testing.T) {
}
tokenString, err := c.CreateJWT(other, "refresh", "foo:read")
assert.NoError(t, err)
_, ok := c.ValidToken(in, RefreshTokenAudience, tokenString)
_, ok := c.ValidToken(in, permissions.RefreshTokenAudience, tokenString)
assert.False(t, ok, "The token should be invalid")
}

Expand All @@ -74,6 +75,6 @@ func TestParseJWTInvalidSubject(t *testing.T) {
}
tokenString, err := other.CreateJWT(in, "refresh", "foo:read")
assert.NoError(t, err)
_, ok := c.ValidToken(in, RefreshTokenAudience, tokenString)
_, ok := c.ValidToken(in, permissions.RefreshTokenAudience, tokenString)
assert.False(t, ok, "The token should be invalid")
}
23 changes: 23 additions & 0 deletions pkg/permissions/claims.go
@@ -0,0 +1,23 @@
package permissions

import jwt "gopkg.in/dgrijalva/jwt-go.v3"

const (
// ContextAudience is the audience for JWT used by apps
ContextAudience = "context"

// RegistrationTokenAudience is the audience field of JWT for registration tokens
RegistrationTokenAudience = "registration"

// AccessTokenAudience is the audience field of JWT for access tokens
AccessTokenAudience = "access"

// RefreshTokenAudience is the audience field of JWT for refresh tokens
RefreshTokenAudience = "refresh"
)

// Claims is used for JWT used in OAuth2 flow and applications token
type Claims struct {
jwt.StandardClaims
Scope string `json:"scope,omitempty"`
}
12 changes: 12 additions & 0 deletions pkg/permissions/errors.go
@@ -0,0 +1,12 @@
package permissions

import "errors"

var (
// ErrInvalidToken is used when the token is invalid (the signature is not
// correct, the domain is not the good one, etc.)
ErrInvalidToken = errors.New("Invalid JWT token")

// ErrInvalidAudience is used when the audience is not expected
ErrInvalidAudience = errors.New("Invalid audience for JWT token")
)