Skip to content

Commit e3a53c6

Browse files
committed
added “single login” feature
1 parent 289ce81 commit e3a53c6

8 files changed

+80
-10
lines changed

README.md

+2-2
Original file line numberDiff line numberDiff line change
@@ -82,9 +82,9 @@ func main() {
8282

8383
```
8484

85-
In order to use basicserver, you need to at least provide `MongoString` and `ServerPort` [configuration options](https://github.com/bonnevoyager/basicserver/blob/master/main.go#L21-L35) to `basicserver.CreateApp(settings)`.
85+
In order to use basicserver, you need to at least provide `MongoString` and `ServerPort` [configuration options](https://github.com/bonnevoyager/basicserver/blob/master/main.go#L21-L49) to `basicserver.CreateApp(settings)`.
8686

87-
Preconfigured [routes](https://github.com/bonnevoyager/basicserver/blob/master/routes.go#L7-L17) are:
87+
Preconfigured [routes](https://github.com/bonnevoyager/basicserver/blob/master/routes.go#L7-L20) are:
8888

8989
- [POST /register](https://github.com/bonnevoyager/basicserver/blob/master/register_post.go)
9090
- [POST /signin](https://github.com/bonnevoyager/basicserver/blob/master/signin_post.go)

keepalive_get.go

+6-2
Original file line numberDiff line numberDiff line change
@@ -32,10 +32,14 @@ func (app *BasicApp) ServeKeepAliveGet() iris.Handler {
3232
uid := ctx.Values().Get("uid").(string)
3333

3434
expiresAt := time.Now().Add(time.Hour * time.Duration(72)).Unix() // 72 hours
35-
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
35+
claimsMap := jwt.MapClaims{
3636
"uid": uid,
3737
"exp": expiresAt,
38-
})
38+
}
39+
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claimsMap)
40+
if app.Settings.SingleLogin { // substain single login value
41+
claimsMap["sl"] = ctx.Values().Get("sl").(string)
42+
}
3943
tokenString, err := token.SignedString(app.Settings.Secret)
4044
if err != nil {
4145
log.Print(err)

main.go

+2
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ type SMTPSettings struct {
3333
// `LogLevel` - available values are: "disable", "fatal", "error", "warn", "info", "debug"
3434
// `MongoString` - URI format described at http://docs.mongodb.org/manual/reference/connection-string/
3535
// `Secret` - secret value used by JWT parser
36+
// `SingleLogin` - allows to access restricted resources only with fresh token received from signin
3637
// `ServerPort` - port on which the server should listen to
3738
// `RecoverTemplate` - html content to be sent along with password recovery email
3839
// `SMTP` - SMTP configuration to send emails
@@ -41,6 +42,7 @@ type Settings struct {
4142
LogLevel string
4243
MongoString string
4344
Secret []byte
45+
SingleLogin bool
4446
ServerPort string
4547
RecoverTemplate string
4648
SMTP SMTPSettings

main_test.go

+38
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package basicserver
22

33
import (
4+
"strconv"
45
"testing"
56
"time"
67

@@ -308,3 +309,40 @@ func TestApiFile(t *testing.T) {
308309

309310
app.Coll.Files.Remove(testUID.Hex() + ":golang.jpg")
310311
}
312+
313+
func TestSingleLogin(t *testing.T) {
314+
e := httptest.New(t, app.Iris)
315+
316+
app.Settings.SingleLogin = true
317+
318+
createTestUser()
319+
320+
timeNow := time.Now()
321+
expiresAt := timeNow.Add(time.Minute * time.Duration(1)).Unix()
322+
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
323+
"uid": testUID.Hex(),
324+
"exp": expiresAt,
325+
"sl": strconv.FormatInt(timeNow.Unix(), 10),
326+
})
327+
tokenString, _ := token.SignedString([]byte(testSecret))
328+
329+
app.Coll.Users.UpdateId(testUID, bson.M{
330+
"$set": bson.M{"last_login_at": timeNow},
331+
})
332+
333+
e.GET("/api/data").
334+
WithHeader("Authorization", "Bearer "+tokenString).
335+
Expect().Status(httptest.StatusOK)
336+
337+
app.Coll.Users.UpdateId(testUID, bson.M{
338+
"$set": bson.M{"last_login_at": timeNow.Add(time.Minute)},
339+
})
340+
341+
e.GET("/api/data").
342+
WithHeader("Authorization", "Bearer "+tokenString).
343+
Expect().Status(httptest.StatusConflict)
344+
345+
removeTestUser()
346+
347+
app.Settings.SingleLogin = false
348+
}

require_auth.go

+12
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package basicserver
22

33
import (
44
"errors"
5+
"strconv"
56
"strings"
67

78
jwt "github.com/dgrijalva/jwt-go"
@@ -18,6 +19,8 @@ import (
1819
// In case of invalid/expired token, this returns status code `401` and `text/plain`
1920
// error message as a response.
2021
//
22+
// In case of single login enabled and locked token provided, this returns status code `409`.
23+
//
2124
func (app *BasicApp) RequireAuth() iris.Handler {
2225
return func(ctx iris.Context) {
2326
authHeader := ctx.GetHeader("Authorization")
@@ -55,10 +58,19 @@ func (app *BasicApp) RequireAuth() iris.Handler {
5558
app.HandleError(err, ctx, iris.StatusInternalServerError)
5659
}
5760
return
61+
} else if app.Settings.SingleLogin && // handle single login
62+
strconv.FormatInt(user.LastLoginAt.Unix(), 10) != token.Claims.(jwt.MapClaims)["sl"] {
63+
err := errors.New("Token Locked")
64+
app.HandleError(err, ctx, iris.StatusConflict)
65+
return
5866
}
5967

6068
// pass on the "uid"
6169
ctx.Values().Set("uid", uid)
70+
if app.Settings.SingleLogin {
71+
sl := token.Claims.(jwt.MapClaims)["sl"]
72+
ctx.Values().Set("sl", sl)
73+
}
6274
ctx.Next()
6375
}
6476
}

signin_post.go

+13-3
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package basicserver
22

33
import (
44
"log"
5+
"strconv"
56
"time"
67

78
jwt "github.com/dgrijalva/jwt-go"
@@ -72,18 +73,27 @@ func (app *BasicApp) ServeSigninPost() iris.Handler {
7273
return
7374
}
7475

75-
expiresAt := time.Now().Add(time.Hour * time.Duration(72)).Unix() // 72 hours
76-
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
76+
timeNow := time.Now()
77+
expiresAt := timeNow.Add(time.Hour * time.Duration(72)).Unix() // 72 hours
78+
claimsMap := jwt.MapClaims{
7779
"uid": user.ID.Hex(),
7880
"exp": expiresAt,
79-
})
81+
}
82+
if app.Settings.SingleLogin { // single login value
83+
claimsMap["sl"] = strconv.FormatInt(timeNow.Unix(), 10)
84+
}
85+
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claimsMap)
8086
tokenString, err := token.SignedString(app.Settings.Secret)
8187
if err != nil {
8288
log.Print(err)
8389
app.HandleError(err, ctx, iris.StatusInternalServerError)
8490
return
8591
}
8692

93+
app.Coll.Users.UpdateId(user.ID, bson.M{
94+
"$set": bson.M{"last_login_at": timeNow},
95+
})
96+
8797
ctx.JSON(iris.Map{
8898
"expires": expiresAt,
8999
"token": tokenString,

state.go

+2-2
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@ import "github.com/globalsign/mgo/bson"
44

55
// State is an user's state data entity:
66
//
7-
// `ID` is user uid
8-
// `Data` is user data
7+
// `ID` user uid
8+
// `Data` user data
99
//
1010
type State struct {
1111
ID bson.ObjectId `bson:"_id" json:"id"`

user.go

+5-1
Original file line numberDiff line numberDiff line change
@@ -2,21 +2,25 @@ package basicserver
22

33
import (
44
"math/rand"
5+
"time"
56

67
"github.com/globalsign/mgo/bson"
78
)
89

910
// User is an user data entity:
1011
//
11-
// `ID` is user uid
12+
// `ID` user uid
1213
// `Email` user email
1314
// `Password` encrypted password
15+
// `RecoveryCode` optional recovery code for password reset
16+
// `LastLoginAt` time at which last login happened
1417
//
1518
type User struct {
1619
ID bson.ObjectId `bson:"_id" json:"id"`
1720
Email string `bson:"email"`
1821
Password string `bson:"password"`
1922
RecoveryCode string `bson:"recovery_code"`
23+
LastLoginAt time.Time `bson:"last_login_at"`
2024
}
2125

2226
var codeLetters = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ")

0 commit comments

Comments
 (0)