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

JWT Auth module #16

Open
wants to merge 18 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions jwtauth/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# Compiled Object files, Static and Dynamic libs (Shared Objects)
*.o
*.a
*.so

# Folders
_obj
_test

# Architecture specific extensions/prefixes
*.[568vq]
[568vq].out

*.cgo1.go
*.cgo2.c
_cgo_defun.c
_cgo_gotypes.go
_cgo_export.*

_testmain.go

*.exe
*.test
*.prof

# revel framework
test-results/
tmp/
routes/

*.sublime-workspace
107 changes: 107 additions & 0 deletions jwtauth/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
# JWT Auth Module for Revel Framework

Pluggable and easy to use JWT auth module in Revel Framework. Module supports following JWT signing method `HS256, HS384, HS512, RS256, RS384, RS512, ES256, ES384, ES512`. Default is `RS512`.

Include [example application](example/jwtauth-example) and it demostrates above mentioned JWT signing method(s).

Planning to bring following enhancement to this moudle:
* Module error messages via Revel messages `/messages/<filename>.en, etc`

### Module Configuration
```ini
# default is REVEL-JWT-AUTH
auth.jwt.realm.name = "JWT-AUTH"

# use appropriate values (string, URL), default is REVEL-JWT-AUTH
auth.jwt.issuer = "JWT AUTH"

# In minutes, default is 60 minutes
auth.jwt.expiration = 30

# Signing Method
# options are - HS256, HS384, HS512, RS256, RS384, RS512, ES256, ES384, ES512
auth.jwt.sign.method = "RS512"

# RSA key files
# applicable to RS256, RS384, RS512 signing method and comment out others
auth.jwt.key.private = "/Users/jeeva/rsa_private.pem"
auth.jwt.key.public = "/Users/jeeva/rsa_public.pem"

# ECDSA key files
# Uncomment below two lines for ES256, ES384, ES512 signing method and comment out others
#auth.jwt.key.private = "/Users/jeeva/ec_private.pem"
#auth.jwt.key.public = "/Users/jeeva/ec_public.pem"

# HMAC signing Secret value
# Uncomment below line for HS256, HS384, HS512 signing method and comment out others
#auth.jwt.key.hmac = "1A39B778C0CE40B1B32585CF846F61B1"

# Valid regexp allowed for path
# Internally it will end up like this "^(/$|/token|/register|/(forgot|validate-reset|reset)-password)"
auth.jwt.anonymous = "/, /token, /register, /(forgot|validate-reset|reset)-password, /freepass/.*"
```

### Enabling Auth Module

Add following into `conf/app.conf` revel app configuration
```ini
# Enabling JWT Auth module
module.jwtauth = github.com/jeevatkm/jwtauth
```

### Registering Auth Routes

Add following into `conf/routes`.
```sh
# Adding JWT Auth routes into application
module:jwtauth
```
JWT Auth modules enables following routes-
```sh
# JWT Auth Routes
POST /token JwtAuth.Token
GET /refresh-token JwtAuth.RefreshToken
GET /logout JwtAuth.Logout
```

### Registering Auth Filter

Revel Filter for JWT Auth Token verification. Register it in the `revel.Filters` in `<APP_PATH>/app/init.go`

```go
// Add jwt.AuthFilter anywhere deemed appropriate, it must be register after revel.PanicFilter
revel.Filters = []revel.Filter{
revel.PanicFilter,
...
jwt.AuthFilter, // JWT Auth Token verification for Request Paths
...
}
// Note: If everything looks good then Claims map made available via c.Args
// and can be accessed using c.Args[jwt.TokenClaimsKey]
```

### Registering Auth Handler

Auth handler is responsible for validate user and returning `Subject (aka sub)` value and success/failure boolean. It should comply [AuthHandler](https://github.com/jeevatkm/jwtauth/blob/master/app/jwt/jwt.go#L31) interface or use raw func via [jwt.AuthHandlerFunc](https://github.com/jeevatkm/jwtauth/blob/master/app/jwt/jwt.go#L37).
```go
revel.OnAppStart(func() {
jwt.Init(jwt.AuthHandlerFunc(func(username, password string) (string, bool) {

// This method will be invoked by JwtAuth module for authentication
// Call your implementation to authenticate user
revel.INFO.Printf("Username: %v, Password: %v", username, password)

// ....
// ....

// after successful authentication
// create User subject value, which you want to inculde in signed string
// such as User Id, user email address, etc.

userId := 100001
authenticated := true // Auth success

return fmt.Sprintf("%d", userId), authenticated
}))
})
```
110 changes: 110 additions & 0 deletions jwtauth/app/controllers/jwtauth.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
package controllers

import (
"encoding/json"
"net/http"
"time"

"github.com/revel/modules/jwtauth/app/jwt"
"github.com/revel/modules/jwtauth/app/models"

"github.com/revel/revel"
"github.com/revel/revel/cache"
)

type JwtAuth struct {
*revel.Controller
}

func (c *JwtAuth) Token() revel.Result {
user, err := c.parseUserInfo()
if err != nil {
revel.ERROR.Printf("Unable to read user info %q", err)
c.Response.Status = http.StatusBadRequest
return c.RenderJson(map[string]string{
"id": "bad_request",
"message": "Unable to read user info",
})
}

if subject, pass := jwt.Authenticate(user.Username, user.Password); pass {
token, err := jwt.GenerateToken(subject)
if err != nil {
c.Response.Status = http.StatusInternalServerError
return c.RenderJson(map[string]string{
"id": "server_error",
"message": "Unable to generate token",
})
}

return c.RenderJson(map[string]string{
"token": token,
})
}

c.Response.Status = http.StatusUnauthorized
c.Response.Out.Header().Set("Www-Authenticate", jwt.Realm)

return c.RenderJson(map[string]string{
"id": "unauthorized",
"message": "Invalid credentials",
})
}

func (c *JwtAuth) RefreshToken() revel.Result {
claims := c.Args[jwt.TokenClaimsKey].(map[string]interface{})
revel.INFO.Printf("Claims: %q", claims)

tokenString, err := jwt.GenerateToken(claims[jwt.SubjectKey].(string))
if err != nil {
c.Response.Status = http.StatusInternalServerError
return c.RenderJson(map[string]string{
"id": "server_error",
"message": "Unable to generate token",
})
}

// Issued new token and adding existing token into blocklist for remaining validitity period
// Let's say if existing token is valid for another 10 minutes, then it reside 10 mintues
// in the blocklist
go addToBlocklist(c.Request, claims)

return c.RenderJson(map[string]string{
"token": tokenString,
})
}

func (c *JwtAuth) ValidateToken() revel.Result {
// When request reaches here, then it has valid auth token
// else request would have received 401 - Unauthorized response
return c.RenderJson(map[string]string{
"id": "success",
"message": "Auth token is valid",
})
}

func (c *JwtAuth) Logout() revel.Result {
// Auth token will be added to blocklist for remaining token validitity period
// Let's token is valid for another 10 minutes, then it reside 10 mintues in the blocklist
go addToBlocklist(c.Request, c.Args[jwt.TokenClaimsKey].(map[string]interface{}))

return c.RenderJson(map[string]string{
"id": "success",
"message": "Successfully logged out",
})
}

// Private methods
func (c *JwtAuth) parseUserInfo() (*models.JwtUser, error) {
rUser := &models.JwtUser{}
decoder := json.NewDecoder(c.Request.Body)
err := decoder.Decode(rUser)
return rUser, err
}

func addToBlocklist(r *revel.Request, claims map[string]interface{}) {
tokenString := jwt.GetAuthToken(r)
expriyAt := time.Minute * time.Duration(jwt.TokenRemainingValidity(claims[jwt.ExpirationKey]))

cache.Set(tokenString, tokenString, expriyAt)
}
Loading