diff --git a/jwtauth/.gitignore b/jwtauth/.gitignore new file mode 100644 index 0000000..5b59b1a --- /dev/null +++ b/jwtauth/.gitignore @@ -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 \ No newline at end of file diff --git a/jwtauth/README.md b/jwtauth/README.md new file mode 100644 index 0000000..550bb7f --- /dev/null +++ b/jwtauth/README.md @@ -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/.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/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 + })) +}) +``` diff --git a/jwtauth/app/controllers/jwtauth.go b/jwtauth/app/controllers/jwtauth.go new file mode 100644 index 0000000..51d8b10 --- /dev/null +++ b/jwtauth/app/controllers/jwtauth.go @@ -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) +} diff --git a/jwtauth/app/jwt/jwt.go b/jwtauth/app/jwt/jwt.go new file mode 100644 index 0000000..db04531 --- /dev/null +++ b/jwtauth/app/jwt/jwt.go @@ -0,0 +1,392 @@ +package jwt + +import ( + "crypto/ecdsa" + "crypto/rsa" + "fmt" + "io/ioutil" + "net/http" + "regexp" + "strings" + "time" + + "github.com/revel/revel" + "github.com/revel/revel/cache" + "gopkg.in/dgrijalva/jwt-go.v2" +) + +const ( + Algorithm = "alg" + IssueKey = "iss" + IssuedAtKey = "iat" + ExpirationKey = "exp" + SubjectKey = "sub" + ExpireOffset = 3600 + TokenClaimsKey = "jwt.auth.claims" +) + +// Objects implementing the AuthHandler interface can be +// registered to Authenticate User for application +type AuthHandler interface { + Authenticate(username, password string) (string, bool) +} + +// The AuthHandlerFunc type is an adapter to allow the use of +// ordinary functions as Auth handlers. +type AuthHandlerFunc func(string, string) (string, bool) + +// Authenticate calls f(u, p). +func (f AuthHandlerFunc) Authenticate(u, p string) (string, bool) { + return f(u, p) +} + +var ( + Realm string + issuer string + expiration int // in minutues + isIssuerExists bool + handler AuthHandler + anonymousPaths *regexp.Regexp + signMethodName string + + // RS* signing elements + jwtAuthSignMethodRSA *jwt.SigningMethodRSA + rsaPrivateKey *rsa.PrivateKey + rsaPublicKey *rsa.PublicKey + + // ES* signing elements + jwtAuthSignMethodECDSA *jwt.SigningMethodECDSA + ecPrivatekey *ecdsa.PrivateKey + ecPublickey *ecdsa.PublicKey + + // HS* signing elements + jwtAuthSignMethodHMAC *jwt.SigningMethodHMAC + hmacSecretBytes []byte +) + +/* +Method Init initializes JWT auth provider based on given config values from app.conf + // + // JWT Auth Module configuration + // + // 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" + + // Uncomment below two lines for ES256, ES384, ES512 signing method and comment out others + // since we will be use ECDSA + #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" +*/ +func Init(authHandler interface{}) { + var ( + found bool + err error + ) + Realm = revel.Config.StringDefault("auth.jwt.realm.name", "REVEL-JWT-AUTH") + issuer = revel.Config.StringDefault("auth.jwt.issuer", "REVEL-JWT-AUTH") + expiration = revel.Config.IntDefault("auth.jwt.expiration", 60) // Default 60 minutes + signMethodName = revel.Config.StringDefault("auth.jwt.sign.method", "RS512") //Default is RS512 + + if strings.HasPrefix(signMethodName, "RS") || strings.HasPrefix(signMethodName, "ES") { + var ( + privateKeyPath string + publicKeyPath string + ) + privateKeyPath, found = revel.Config.String("auth.jwt.key.private") + if !found { + revel.ERROR.Fatal("No auth.jwt.key.private found, it's required for RS*/ES* signing method.") + } + + publicKeyPath, found = revel.Config.String("auth.jwt.key.public") + if !found { + revel.ERROR.Fatal("No auth.jwt.key.public found, it's required for RS*/ES* signing method.") + } + + if strings.HasPrefix(signMethodName, "RS") { + // loading RSA key files + rsaPrivateKey, err = jwt.ParseRSAPrivateKeyFromPEM(readFile(privateKeyPath)) + if err != nil { + revel.ERROR.Fatalf("Private key file read error [%v].", privateKeyPath) + } + + rsaPublicKey, err = jwt.ParseRSAPublicKeyFromPEM(readFile(publicKeyPath)) + if err != nil { + revel.ERROR.Fatalf("Public key file read error [%v].", publicKeyPath) + } + + // Signing Method + jwtAuthSignMethodRSA = identifySigningMethod().(*jwt.SigningMethodRSA) + } else if strings.HasPrefix(signMethodName, "ES") { + // loading ECDSA key files + ecPrivatekey, err = jwt.ParseECPrivateKeyFromPEM(readFile(privateKeyPath)) + if err != nil { + revel.ERROR.Fatalf("Private key file read error [%v].", privateKeyPath) + } + + ecPublickey, err = jwt.ParseECPublicKeyFromPEM(readFile(publicKeyPath)) + if err != nil { + revel.ERROR.Fatalf("Public key file read error [%v].", publicKeyPath) + } + + // Signing Method + jwtAuthSignMethodECDSA = identifySigningMethod().(*jwt.SigningMethodECDSA) + } + } + + if strings.HasPrefix(signMethodName, "HS") { + var hmacSecertValue string + hmacSecertValue, found = revel.Config.String("auth.jwt.key.hmac") + if !found || len(strings.TrimSpace(hmacSecertValue)) == 0 { + revel.ERROR.Fatal("No auth.jwt.key.hmac found, it's required for HS* signing method.") + } + + // Signing Method + jwtAuthSignMethodHMAC = identifySigningMethod().(*jwt.SigningMethodHMAC) + hmacSecretBytes = []byte(hmacSecertValue) + } + + if _, ok := authHandler.(AuthHandler); !ok { + revel.ERROR.Fatal("Auth Handler doesn't implement interface jwt.AuthHandler") + } + + Realm = fmt.Sprintf(`Bearer realm="%s"`, Realm) + + // preparing anonymous path regex + anonymous := revel.Config.StringDefault("auth.jwt.anonymous", "/token") + paths := strings.Split(anonymous, ",") + regexString := "" + for _, p := range paths { + if strings.TrimSpace(p) == "/" { // TODO Something not right, Might need a revist here + regexString = fmt.Sprintf("%s%s$|", regexString, strings.TrimSpace(p)) + } else { + regexString = fmt.Sprintf("%s%s|", regexString, strings.TrimSpace(p)) + } + } + regexString = fmt.Sprintf("^(%s)", regexString[:len(regexString)-1]) + anonymousPaths = regexp.MustCompile(regexString) + + isIssuerExists = len(issuer) > 0 + handler = authHandler.(AuthHandler) + + revel.INFO.Printf("JWT Auth Module - Signing Method: %v", signMethodName) +} + +// Method GenerateToken creates JWT signed string with given subject value +func GenerateToken(subject string) (string, error) { + token := createToken() + + token.Claims[IssueKey] = issuer + token.Claims[IssuedAtKey] = time.Now().Unix() + token.Claims[ExpirationKey] = time.Now().Add(time.Minute * time.Duration(expiration)).Unix() + token.Claims[SubjectKey] = subject + + return signString(token) +} + +// Method ParseFromRequest retrives JWT token, validates against SigningMethod & Issuer +// then returns *jwt.Token object +func ParseFromRequest(req *http.Request) (*jwt.Token, error) { + return jwt.ParseFromRequest(req, func(token *jwt.Token) (interface{}, error) { + // Signing Method verification + // https://auth0.com/blog/2015/03/31/critical-vulnerabilities-in-json-web-token-libraries/ + var signMethodOk bool + var key interface{} + if strings.HasPrefix(signMethodName, "RS") { + _, signMethodOk = token.Method.(*jwt.SigningMethodRSA) + key = rsaPublicKey + } else if strings.HasPrefix(signMethodName, "HS") { + _, signMethodOk = token.Method.(*jwt.SigningMethodHMAC) + key = hmacSecretBytes + } else if strings.HasPrefix(signMethodName, "ES") { + _, signMethodOk = token.Method.(*jwt.SigningMethodECDSA) + key = ecPublickey + } + if !signMethodOk { + return nil, fmt.Errorf("Unexpected signing method: %v", token.Header[Algorithm]) + } + + // Issuer verification + if token.Claims[IssueKey] != issuer { + return nil, fmt.Errorf("Unexpected token issuer: %v", token.Claims[IssueKey]) + } + + return key, nil + }) +} + +// Method TokenRemainingValidity calculates the remaining time left out in auth token +func TokenRemainingValidity(timestamp interface{}) int { + if validity, ok := timestamp.(float64); ok { + tm := time.Unix(int64(validity), 0) + remainer := tm.Sub(time.Now()) + if remainer > 0 { + return int(remainer.Seconds() + ExpireOffset) + } + } + + return ExpireOffset +} + +func Authenticate(username, password string) (string, bool) { + return handler.Authenticate(username, password) +} + +// Method GetAuthToken retrives Auth Token from revel.Request +// Authorization: Bearer +func GetAuthToken(req *revel.Request) string { + authToken := req.Header.Get("Authorization") + + if len(authToken) > 7 { // char count "Bearer " ==> 7 + return authToken[7:] + } + + return "" +} + +// Method IsInBlocklist is checks against logged out tokens +func IsInBlocklist(token string) bool { + var existingToken string + cache.Get(token, &existingToken) + + if len(existingToken) > 0 { + revel.WARN.Printf("Yes, blocklisted token [%v]", existingToken) + return true + } + + return false +} + +/* +Filter AuthFilter is Revel Filter for JWT Auth Token verification +Register it in the revel.Filters in /app/init.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] +*/ +func AuthFilter(c *revel.Controller, fc []revel.Filter) { + if anonymousPaths.MatchString(c.Request.URL.Path) { + fc[0](c, fc[1:]) //not applying JWT auth filter due to anonymous path + } else { + token, err := ParseFromRequest(c.Request.Request) + if err == nil && token.Valid && !IsInBlocklist(GetAuthToken(c.Request)) { + c.Args[TokenClaimsKey] = token.Claims + + fc[0](c, fc[1:]) // everything looks good, move on + } else { + if ve, ok := err.(*jwt.ValidationError); ok { + if ve.Errors&jwt.ValidationErrorMalformed != 0 { + revel.ERROR.Println("That's not even a token") + } else if ve.Errors&(jwt.ValidationErrorExpired|jwt.ValidationErrorNotValidYet) != 0 { + revel.ERROR.Println("Timing is everything, Token is either expired or not active yet") + } else { + revel.ERROR.Printf("Couldn't handle this request: %v", err) + } + } else { + revel.ERROR.Printf("Couldn't handle this request: %v", err) + } + + c.Response.Status = http.StatusUnauthorized + c.Response.Out.Header().Add("Www-Authenticate", Realm) + c.Result = c.RenderJson(map[string]string{ + "id": "unauthorized", + "message": "Invalid or token is not provided", + }) + + return + } + } +} + +// +// Private Methods +// +func createToken() *jwt.Token { + if strings.HasPrefix(signMethodName, "HS") { + return jwt.New(jwtAuthSignMethodHMAC) + } else if strings.HasPrefix(signMethodName, "ES") { + return jwt.New(jwtAuthSignMethodECDSA) + } + + return jwt.New(jwtAuthSignMethodRSA) +} + +func signString(token *jwt.Token) (signedString string, err error) { + switch signMethodName { + case "RS256", "RS384", "RS512": + signedString, err = token.SignedString(rsaPrivateKey) + case "HS256", "HS384", "HS512": + signedString, err = token.SignedString(hmacSecretBytes) + case "ES256", "ES384", "ES512": + signedString, err = token.SignedString(ecPrivatekey) + } + + if err != nil { + revel.ERROR.Printf("Generate token error [%v]", err) + } + + return +} + +func readFile(file string) []byte { + bytes, err := ioutil.ReadFile(file) + if err != nil { + revel.ERROR.Fatalf("Key file read error [%v]", file) + } + return bytes +} + +func identifySigningMethod() (signingMethod interface{}) { + switch signMethodName { + case "RS256": + signingMethod = jwt.SigningMethodRS256 + case "RS384": + signingMethod = jwt.SigningMethodRS384 + case "RS512": + signingMethod = jwt.SigningMethodRS512 + case "HS256": + signingMethod = jwt.SigningMethodHS256 + case "HS384": + signingMethod = jwt.SigningMethodHS384 + case "HS512": + signingMethod = jwt.SigningMethodHS512 + case "ES256": + signingMethod = jwt.SigningMethodES256 + case "ES384": + signingMethod = jwt.SigningMethodES384 + case "ES512": + signingMethod = jwt.SigningMethodES512 + } + return +} diff --git a/jwtauth/app/models/jwtuser.go b/jwtauth/app/models/jwtuser.go new file mode 100644 index 0000000..685dbef --- /dev/null +++ b/jwtauth/app/models/jwtuser.go @@ -0,0 +1,6 @@ +package models + +type JwtUser struct { + Username string `json:"username"` + Password string `json:"password"` +} diff --git a/jwtauth/conf/routes b/jwtauth/conf/routes new file mode 100644 index 0000000..9e3f469 --- /dev/null +++ b/jwtauth/conf/routes @@ -0,0 +1,5 @@ +# JWT Auth Routes +POST /token JwtAuth.Token +GET /refresh-token JwtAuth.RefreshToken +GET /validate JwtAuth.ValidateToken +GET /logout JwtAuth.Logout diff --git a/jwtauth/example/jwtauth-example/.gitignore b/jwtauth/example/jwtauth-example/.gitignore new file mode 100644 index 0000000..dae67d0 --- /dev/null +++ b/jwtauth/example/jwtauth-example/.gitignore @@ -0,0 +1,3 @@ +test-results/ +tmp/ +routes/ diff --git a/jwtauth/example/jwtauth-example/README.md b/jwtauth/example/jwtauth-example/README.md new file mode 100644 index 0000000..aa9f16e --- /dev/null +++ b/jwtauth/example/jwtauth-example/README.md @@ -0,0 +1,83 @@ +## JWT Auth Module Demo Revel Application + +Ready to see JWT Auth module in action. It demonstrates the JWT Auth Module usage. + +### How to run? +```sh +$ cd $GOPATH + +$ go get github.com/revel/modules + +$ revel run github.com/revel/modules/jwtauth/example/jwtauth-example + +# now application will be running http://localhost:9000 +``` + +### Sample Application in action +JWT Auth Module provides following routes for Revel application. It is perfectly ready to use for REST API, Web application and Single Page web application. +```sh +# JWT Auth Routes +POST /token JwtAuth.Token +GET /refresh-token JwtAuth.RefreshToken +GET /logout JwtAuth.Logout +``` +Sample application has three user information, such as- +```sh +Username: jeeva@myjeeva.com +Password: sample1 + +Username: user1@myjeeva.com +Password: user1 + +Username: user2@myjeeva.com +Password: user2 +``` + +#### /token +Let's get the auth token, pick any rest client by your choice and user credentials from above. + +| Field | Value | +| ------------- | ------------- | +| URL | http://localhost:9000/token | +| Method | POST | +| Body | ` { "username":"jeeva@myjeeva.com", "password":"sample1" } ` | +| Response | ` { "token": "" } ` | + +#### /refresh-oken +Let's get refereshed auth token. **Once you execute this request, your previous auth token is no longer valid.** + +| Field | Value | +| ------------- | ------------- | +| URL |http://localhost:9000/refresh-token | +| Method | GET | +| Header | ` Authorization: Bearer ` | +| Response | ` { "token": "" } ` | + +#### /logout +I'm done, let's logout from application. + +| Field | Value | +| ------------- | ------------- | +| URL |http://localhost:9000/logout | +| Method | GET | +| Header | ` Authorization: Bearer ` | +| Response | ` { "id": "success", "message": "Successfully logged out" } ` | + + +### How to create RSA Key files? +```sh +# generating private key file +$ openssl genrsa -out rsa_private.pem 2048 + +# creating a public from private key we generated above +$ openssl rsa -in rsa_private.pem -pubout > rsa_public.pem +``` + +### How to create ECDSA Key files? +```sh +# generating private key file +$ openssl ecparam -name secp384r1 -genkey -noout -out ec_private.pem + +# creating a public from private key we generated above +$ openssl ec -in ec_private.pem -pubout -out ec_public.pem +``` diff --git a/jwtauth/example/jwtauth-example/app/controllers/app.go b/jwtauth/example/jwtauth-example/app/controllers/app.go new file mode 100644 index 0000000..d6eabc9 --- /dev/null +++ b/jwtauth/example/jwtauth-example/app/controllers/app.go @@ -0,0 +1,96 @@ +package controllers + +import ( + "fmt" + + "github.com/revel/modules/jwtauth/app/jwt" + "github.com/revel/modules/jwtauth/example/jwtauth-example/app/models" + "github.com/revel/revel" +) + +// For demo purpose +var appUsers map[string]*models.User + +type App struct { + *revel.Controller +} + +func (c App) Index() revel.Result { + return c.Render() +} + +func (c *App) Register() revel.Result { + return c.RenderJson(map[string]string{ + "message": "You have reached REGISTER route via POST method", + }) +} + +func (c *App) ForgotPassword() revel.Result { + return c.RenderJson(map[string]string{ + "message": "You have reached FORGOT PASSWORD route via POST method", + }) +} + +func (c *App) ResetPassword() revel.Result { + return c.RenderJson(map[string]string{ + "message": "You have reached RESET PASSWORD route via POST method", + }) +} + +func init() { + // Creating couple of users for example application + appUsers = make(map[string]*models.User) + + appUsers["100001"] = &models.User{ + Id: 100001, + Email: "jeeva@myjeeva.com", + Password: "sample1", + FirstName: "Jeeva", + LastName: "M", + } + + appUsers["100002"] = &models.User{ + Id: 100001, + Email: "user1@myjeeva.com", + Password: "user1", + FirstName: "User", + LastName: "1", + } + + appUsers["100003"] = &models.User{ + Id: 100001, + Email: "user2@myjeeva.com", + Password: "user2", + FirstName: "User", + LastName: "2", + } + + 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. + + // Note: using plain password for demo purpose + var userId int64 + var authenticated bool + for _, v := range appUsers { + if v.Email == username && v.Password == password { + userId = v.Id + authenticated = true + break + } + } + + return fmt.Sprintf("%d", userId), authenticated + })) + }) +} diff --git a/jwtauth/example/jwtauth-example/app/init.go b/jwtauth/example/jwtauth-example/app/init.go new file mode 100644 index 0000000..6f4b786 --- /dev/null +++ b/jwtauth/example/jwtauth-example/app/init.go @@ -0,0 +1,42 @@ +package app + +import ( + "github.com/revel/modules/jwtauth/app/jwt" + "github.com/revel/revel" +) + +func init() { + // Filters is the default set of global filters. + revel.Filters = []revel.Filter{ + revel.PanicFilter, // Recover from panics and display an error page instead. + revel.RouterFilter, // Use the routing table to select the right Action + revel.FilterConfiguringFilter, // A hook for adding or removing per-Action filters. + revel.ParamsFilter, // Parse parameters into Controller.Params. + jwt.AuthFilter, // JWT Auth Token verification for Request Paths + revel.SessionFilter, // Restore and write the session cookie. + revel.FlashFilter, // Restore and write the flash cookie. + revel.ValidationFilter, // Restore kept validation errors and save new ones from cookie. + revel.I18nFilter, // Resolve the requested language + HeaderFilter, // Add some security based headers + revel.InterceptorFilter, // Run interceptors around the action. + revel.CompressFilter, // Compress the result. + revel.ActionInvoker, // Invoke the action. + } + + // register startup functions with OnAppStart + // ( order dependent ) + // revel.OnAppStart(InitDB) + // revel.OnAppStart(FillCache) +} + +// TODO turn this into revel.HeaderFilter +// should probably also have a filter for CSRF +// not sure if it can go in the same filter or not +var HeaderFilter = func(c *revel.Controller, fc []revel.Filter) { + // Add some common security headers + c.Response.Out.Header().Add("X-Frame-Options", "SAMEORIGIN") + c.Response.Out.Header().Add("X-XSS-Protection", "1; mode=block") + c.Response.Out.Header().Add("X-Content-Type-Options", "nosniff") + + fc[0](c, fc[1:]) // Execute the next filter stage. +} diff --git a/jwtauth/example/jwtauth-example/app/models/user.go b/jwtauth/example/jwtauth-example/app/models/user.go new file mode 100644 index 0000000..70286b0 --- /dev/null +++ b/jwtauth/example/jwtauth-example/app/models/user.go @@ -0,0 +1,11 @@ +package models + +type User struct { + Id int64 + Email string + Password string + FirstName string + LastName string + + // so on... +} diff --git a/jwtauth/example/jwtauth-example/app/views/app/index.html b/jwtauth/example/jwtauth-example/app/views/app/index.html new file mode 100644 index 0000000..5a1c3bb --- /dev/null +++ b/jwtauth/example/jwtauth-example/app/views/app/index.html @@ -0,0 +1 @@ +

Welcome to JWT Auth Example

\ No newline at end of file diff --git a/jwtauth/example/jwtauth-example/conf/app.conf b/jwtauth/example/jwtauth-example/conf/app.conf new file mode 100644 index 0000000..eb1d0e0 --- /dev/null +++ b/jwtauth/example/jwtauth-example/conf/app.conf @@ -0,0 +1,195 @@ +################################################################################ +# Revel configuration file +# See: +# http://revel.github.io/manual/appconf.html +# for more detailed documentation. +################################################################################ + +# This sets the `AppName` variable which can be used in your code as +# `if revel.AppName {...}` +app.name = jwtauth-example + +# A secret string which is passed to cryptographically sign the cookie to prevent +# (and detect) user modification. +# Keep this string secret or users will be able to inject arbitrary cookie values +# into your application +app.secret = 5HX7DyJHNI3PJDEt6EbUx9sZ5kbSSJUhs8rm1S592yoYAnHb28gmucIcDDRrsYQa + + +# The IP address on which to listen. +http.addr = + +# The port on which to listen. +http.port = 9000 + +# Whether to use SSL or not. +http.ssl = false + +# Path to an X509 certificate file, if using SSL. +#http.sslcert = + +# Path to an X509 certificate key, if using SSL. +#http.sslkey = + + +# For any cookies set by Revel (Session,Flash,Error) these properties will set +# the fields of: +# http://golang.org/pkg/net/http/#Cookie +# +# The HttpOnly attribute is supported by most modern browsers. On a supported +# browser, an HttpOnly session cookie will be used only when transmitting HTTP +# (or HTTPS) requests, thus restricting access from other, non-HTTP APIs (such +# as JavaScript). This restriction mitigates, but does not eliminate the threat +# of session cookie theft via cross-site scripting (XSS). This feature applies +# only to session-management cookies, and not other browser cookies. +cookie.httponly = false + +# Each cookie set by Revel is prefixed with this string. +cookie.prefix = REVEL + +# A secure cookie has the secure attribute enabled and is only used via HTTPS, +# ensuring that the cookie is always encrypted when transmitting from client to +# server. This makes the cookie less likely to be exposed to cookie theft via +# eavesdropping. +cookie.secure = false + +# Limit cookie access to a given domain +#cookie.domain = + +# Define when your session cookie expires. Possible values: +# "720h" +# A time duration (http://golang.org/pkg/time/#ParseDuration) after which +# the cookie expires and the session is invalid. +# "session" +# Sets a session cookie which invalidates the session when the user close +# the browser. +session.expires = 720h + + +# The date format used by Revel. Possible formats defined by the Go `time` +# package (http://golang.org/pkg/time/#Parse) +format.date = 01/02/2006 +format.datetime = 01/02/2006 15:04 + + +# Determines whether the template rendering should use chunked encoding. +# Chunked encoding can decrease the time to first byte on the client side by +# sending data before the entire template has been fully rendered. +results.chunked = false + + +# Prefixes for each log message line +log.trace.prefix = "TRACE " +log.info.prefix = "INFO " +log.warn.prefix = "WARN " +log.error.prefix = "ERROR " + + +# The default language of this application. +i18n.default_language = en + + +# Module to serve static content such as CSS, JavaScript and Media files +# Allows Routes like this: +# `Static.ServeModule("modulename","public")` +module.static=github.com/revel/modules/static + +# Enabling JWT Auth module +module.jwtauth = github.com/revel/modules/jwtauth + + + +################################################################################ +# Section: dev +# This section is evaluated when running Revel in dev mode. Like so: +# `revel run path/to/myapp` +[dev] +# This sets `DevMode` variable to `true` which can be used in your code as +# `if revel.DevMode {...}` +# or in your templates with +# `` +mode.dev = true + +# +# JWT Auth Module configuration +# +# 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 = "${GOPATH}/src/github.com/revel/modules/jwtauth/example/jwtauth-example/conf/rsa_private.pem" +auth.jwt.key.public = "${GOPATH}/src/github.com/revel/modules/jwtauth/example/jwtauth-example/conf/rsa_public.pem" + +# Uncomment below two lines for ES256, ES384, ES512 signing method and comment out others +# since we will be use ECDSA +#auth.jwt.key.private = "${GOPATH}/src/github.com/revel/modules/jwtauth/example/jwtauth-example/conf/ec_private.pem" +#auth.jwt.key.public = "${GOPATH}/src/github.com/revel/modules/jwtauth/example/jwtauth-example/conf/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 +auth.jwt.anonymous = "/, /token, /register, /(forgot|reset)-password" + +# Pretty print JSON/XML when calling RenderJson/RenderXml +results.pretty = true + + +# Automatically watches your applicaton files and recompiles on-demand +watch = true + + +# If you set watcher.mode = "eager", the server starts to recompile +# your application every time your application's files change. +watcher.mode = "normal" + + +# Module to run code tests in the browser +# See: +# http://revel.github.io/manual/testing.html +module.testrunner = github.com/revel/modules/testrunner + + +# Where to log the various Revel logs +log.trace.output = off +log.info.output = stderr +log.warn.output = stderr +log.error.output = stderr + + + +################################################################################ +# Section: prod +# This section is evaluated when running Revel in production mode. Like so: +# `revel run path/to/myapp prod` +# See: +# [dev] section for documentation of the various settings +[prod] +mode.dev = false + + +results.pretty = false + + +watch = false + + +module.testrunner = + + +log.trace.output = off +log.info.output = off +log.warn.output = %(app.name)s.log +log.error.output = %(app.name)s.log diff --git a/jwtauth/example/jwtauth-example/conf/ec_private.pem b/jwtauth/example/jwtauth-example/conf/ec_private.pem new file mode 100644 index 0000000..94b5abc --- /dev/null +++ b/jwtauth/example/jwtauth-example/conf/ec_private.pem @@ -0,0 +1,6 @@ +-----BEGIN EC PRIVATE KEY----- +MIGkAgEBBDAtao7AVreEqxzJSQ7tI9aai5r2mH9Lqv1XPH6oS6NyvPjNuIMyilkZ +GmUsKTnfOYegBwYFK4EEACKhZANiAAQz36OH0fGx4jb44EJ6q3DByXb10TzfHov7 +QDZESt/9+p0qfoGvgGlkGPr7ZamROrTuuZM2ezyVUF6Wlexzhnd84chrL9EvGEWI +Erz1EpstgMcuKObSkS8GGBkbNPev3Hw= +-----END EC PRIVATE KEY----- diff --git a/jwtauth/example/jwtauth-example/conf/ec_public.pem b/jwtauth/example/jwtauth-example/conf/ec_public.pem new file mode 100644 index 0000000..d96bf6a --- /dev/null +++ b/jwtauth/example/jwtauth-example/conf/ec_public.pem @@ -0,0 +1,5 @@ +-----BEGIN PUBLIC KEY----- +MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEM9+jh9HxseI2+OBCeqtwwcl29dE83x6L ++0A2RErf/fqdKn6Br4BpZBj6+2WpkTq07rmTNns8lVBelpXsc4Z3fOHIay/RLxhF +iBK89RKbLYDHLijm0pEvBhgZGzT3r9x8 +-----END PUBLIC KEY----- diff --git a/jwtauth/example/jwtauth-example/conf/routes b/jwtauth/example/jwtauth-example/conf/routes new file mode 100644 index 0000000..7453471 --- /dev/null +++ b/jwtauth/example/jwtauth-example/conf/routes @@ -0,0 +1,22 @@ +# Routes +# This file defines all application routes (Higher priority routes first) +# ~~~~ + +module:testrunner + +# Adding JWT Auth routes into application +module:jwtauth + +GET / App.Index +POST /register App.Register +POST /forgot-password App.ForgotPassword +POST /reset-password App.ResetPassword + +# Ignore favicon requests +GET /favicon.ico 404 + +# Map static resources from the /app/public folder to the /public path +GET /public/*filepath Static.Serve("public") + +# Catch all +* /:controller/:action :controller.:action diff --git a/jwtauth/example/jwtauth-example/conf/rsa_private.pem b/jwtauth/example/jwtauth-example/conf/rsa_private.pem new file mode 100644 index 0000000..4a1e7c2 --- /dev/null +++ b/jwtauth/example/jwtauth-example/conf/rsa_private.pem @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEpQIBAAKCAQEA0P3cWR1+vkiXJlrgm3gejMNde62BB/Q4anwdaQ3sTx+E4r3C +iXV27bYcTibYaxJTLrtuRvjpADAplEYsbkBeSAymWEkdUoyRmveUTLBqj60nS8jo +GEpg0QgJ+wLudeaHNsuz/BNHCu/HY+hlZorp0doxt7/mp+vW9+Ii7tGrsRHhlhUj +QVCPWhabPIl9SJERQL49XwU3S9z233GSlt8XRkvhr8YQIVvQOMNccaKkzBeFqORu +JCZ00LvyNZgKfQ+evJ//2C11M/KScx7U/SvOOWP0Ym10GZuSF6jyZZ9o2fGInEvo +Ij14VNgFKtQC4Kl7NVY+pzhGr+EFl9UeaDoplwIDAQABAoIBAA7OXWUG3OrYM7Uo +7Q62pNtuH9paQXDx0Wlh36eIr/wvDHgP349jfgh7RWgYAm8bfj8qUja+/argvqFd +k1pAPy21j7djfqtRgCNNdPk16mbBaq5IzoCiDFfizOo2m/RIX733EopCR18z+5lN +ZpmsL8KJRcpx0wKEh9dJ8xWeTx6dN9A/VWQRzVy7NtOwvdWf3cJzZNLM08Xts2Ad +gBh59M/dRSAHlojHOJ+7mM3JP8OFYUqRG8ihfWKzaWzu+1bSnOKL0/tc2XXeiMCF +N7r5Ym1UEVAcub+8j4UFc6FXTS1sEAJj+Idrh20TCmBGvs2hWhMcPNBV0R8RkkBI +ZIE6vwkCgYEA+oNn77qub8uCESaqvBn1Li+becai5B4BnivAV1UBX7lKsAgDvAwL +u0qOuLEDxrdGMy7movP9B6fs9qP7lkMno/vA+VtbmfvIVQ1Tl8jJus+snKkLbF8u +bZDx5n5HEXT/uduUDMxCCb46WYdX1qXFaEx0wnnmxVG5ABcmPkZhlbMCgYEA1ZGl +/5CaNwQLfmbVrulfqA2ZPOKsLTQPp47B7SE45bXNJTum1tjzmGELF5LaqRuJUuup +TgrDloBge004Fw5LrtJBThmHGcKO/t6ZqEUHMDXOIkll5b0OSxcCcAsh8qy4Knm0 +AGveRetV24j1I3FNZr9KXAAuhgFu6OUNOeprco0CgYEAwFu0rITpOtjGmArb4TIB +bSSLOvfGzmkoDt9Dgwu30VwDOKX+0B9jxr3aV4E9CBJk6hpiaM/3BDDyqPSD0/7e +6nD+3bpD3TpTutNP0+YO2M5smaLILb/sc59vz/A4+/OeBYXQ6f7R2o9iWKqvTRff +PFYw9cAK7orxBlvANuNuPTcCgYEAjA4B0EEiAOY0K2aAxz3gLzMLxPPZeaNkiLuD +zWA2Ed5RdBNUbBzGUq2BOqphnvih67EDzFweu7ngi7uuBuCnHTRhAziWcnw2jkmo +dsMd3a3LSozbt/dtQi0KujNyxdQiyigZtRUIJM4Z9egw6ldJLRJRT1gHKnYSJ8Te +EZb7c5kCgYEA8tw9PO5FrysNLt1X0rVTh/XtEYt8lPlcj1dufbyvU2Gsd4LeYSTc +I8fDQ86rHUbk7uDfzgA7h79UhuaMOapVNpeYlUn5FzkR/ossi8Oa3d5fj2yYPTBW +aSnO0r+ogmgVccO+KZvQcfJ+VmlhG8oRWhhkKmeAHDZv2zSTdaLQ2xI= +-----END RSA PRIVATE KEY----- diff --git a/jwtauth/example/jwtauth-example/conf/rsa_public.pem b/jwtauth/example/jwtauth-example/conf/rsa_public.pem new file mode 100644 index 0000000..320be95 --- /dev/null +++ b/jwtauth/example/jwtauth-example/conf/rsa_public.pem @@ -0,0 +1,9 @@ +-----BEGIN PUBLIC KEY----- +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA0P3cWR1+vkiXJlrgm3ge +jMNde62BB/Q4anwdaQ3sTx+E4r3CiXV27bYcTibYaxJTLrtuRvjpADAplEYsbkBe +SAymWEkdUoyRmveUTLBqj60nS8joGEpg0QgJ+wLudeaHNsuz/BNHCu/HY+hlZorp +0doxt7/mp+vW9+Ii7tGrsRHhlhUjQVCPWhabPIl9SJERQL49XwU3S9z233GSlt8X +Rkvhr8YQIVvQOMNccaKkzBeFqORuJCZ00LvyNZgKfQ+evJ//2C11M/KScx7U/SvO +OWP0Ym10GZuSF6jyZZ9o2fGInEvoIj14VNgFKtQC4Kl7NVY+pzhGr+EFl9UeaDop +lwIDAQAB +-----END PUBLIC KEY----- diff --git a/jwtauth/example/jwtauth-example/messages/sample.en b/jwtauth/example/jwtauth-example/messages/sample.en new file mode 100644 index 0000000..fc447f9 --- /dev/null +++ b/jwtauth/example/jwtauth-example/messages/sample.en @@ -0,0 +1,7 @@ +# Sample messages file for the English language (en) +# Message file extensions should be ISO 639-1 codes (http://en.wikipedia.org/wiki/List_of_ISO_639-1_codes) +# Sections within each message file can optionally override the defaults using ISO 3166-1 alpha-2 codes (http://en.wikipedia.org/wiki/ISO_3166-1_alpha-2) +# See also: +# - http://www.rfc-editor.org/rfc/bcp/bcp47.txt +# - http://www.w3.org/International/questions/qa-accept-lang-locales + diff --git a/jwtauth/example/jwtauth-example/tests/apptest.go b/jwtauth/example/jwtauth-example/tests/apptest.go new file mode 100644 index 0000000..e81ca3d --- /dev/null +++ b/jwtauth/example/jwtauth-example/tests/apptest.go @@ -0,0 +1,21 @@ +package tests + +import "github.com/revel/revel/testing" + +type AppTest struct { + testing.TestSuite +} + +func (t *AppTest) Before() { + println("Set up") +} + +func (t *AppTest) TestThatIndexPageWorks() { + t.Get("/") + t.AssertOk() + t.AssertContentType("text/html; charset=utf-8") +} + +func (t *AppTest) After() { + println("Tear down") +}