~evanj/cms

ref: f2ad8887d95788430ed361e00b18eec1699b2253 cms/vendor/git.sr.ht/~evanj/security/security.go -rw-r--r-- 4.1 KiB
f2ad8887Evan J Feat(vendor): Updating deps. 1 year, 18 days ago
                                                                                
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
package security

import (
	"fmt"

	"github.com/dgrijalva/jwt-go"
	"golang.org/x/crypto/bcrypt"
)

// Security is the security service object. Security presents an easy to use
// interface for common actions needed in CRUD applications such as:
//   1. Creating and comparing hashes (for passwords).
//   2. Creating and reading from tokens (for persistents when sessions are not
//      available).
//
// Usage:
//   // Do this once in your application.
//   sec := security.Default("MY_SECRET_HERE")
//
//   // Create password hash for database
//   hash, err := sec.HashCreate(someSalt, somePass)
//   if err != nil {}
//
//   // Check hash for attempted login.
//   if err := HashCompare(userInput, hash); err != nil {
//     panic("incorrect password")
//   }
//
//   // Create token for user struct.
//   token, err := sec.TokenCreate(TokenMap{"UserID": user.ID()})
//   if err != nil {}
//
//   // Decode user struct token.
//	 claims, err := sec.TokenFrom(token)
//	 if err != nil {}
//	 id, ok := claims["UserID"].(string)
//	 if !ok {}
type Security struct{ secret string }

// Default builds the security service object.
//
// Usage:
//   // Do this once in your application.
//   sec := security.Default("MY_SECRET_HERE")
//
//   // Create password hash for database
//   hash, err := sec.HashCreate(someSalt, somePass)
//   if err != nil {}
//
//   // Check hash for attempted login.
//   if err := HashCompare(userInput, hash); err != nil {
//     panic("incorrect password")
//   }
//
//   // Create token for user struct.
//   token, err := sec.TokenCreate(TokenMap{"UserID": user.ID()})
//   if err != nil {}
//
//   // Decode user struct token.
//	 claims, err := sec.TokenFrom(token)
//	 if err != nil {}
//	 id, ok := claims["UserID"].(string)
//	 if !ok {}
func Default(secret string) Security { return Security{secret} }
func New(secret string) Security     { return Default(secret) }

// HashCreate will create a hash for a password.
func (sec Security) HashCreate(salt, pass string) (string, error) {
	bytes, err := bcrypt.GenerateFromPassword([]byte(salt+pass), 4)
	return string(bytes), err
}

// HashCompare will compare a user input with a previously created hash value.
// Commonly used for password verification while logging users in. Returns an
// error when password differs from the hash input.
func (sec Security) HashCompare(salt, pass, hash string) error {
	return bcrypt.CompareHashAndPassword([]byte(hash), []byte(salt+pass))
}

// TokenMap is the type supplied to and returned from TokenCreate and TokenFrom
// respectively. Use a you would a `map[string]interface{}`. It's an abstraction
// over `jwt.MapClaims` (github.com/dgrijalva/jwt-go) so you don't have to
// import both libraries.
type TokenMap = jwt.MapClaims

// TokenCreate creates a jwt string value given a `TokenMap`. Will return an
// error for unable to create token.
func (sec Security) TokenCreate(val TokenMap) (string, error) { // nolint
	// Not linted because linter complains about `TokenMap` not being
	// `jwt.MapClaims`.
	token := jwt.NewWithClaims(jwt.SigningMethodHS256, val)
	return token.SignedString([]byte(sec.secret))
}

// TokenFrom returns a `TokenMap` item given a jwt string value. Be
// careful, you will need to type check these values at run time like so:
//	 claims, err := sec.TokenFrom(token)
//	 if err != nil {}
//	 id, ok := claims["UserID"].(string)
//	 if !ok {}
//   // no you can use the `id` item stored within the token.
func (sec Security) TokenFrom(tokenString string) (_ret TokenMap, recoverErr error) {
	defer func() {
		if recover() != nil {
			// That's right, jwt will panic on bad input. Don't do this.
			recoverErr = fmt.Errorf("hash has no value")
		}
	}()

	token, err := jwt.Parse(tokenString, 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 []byte(sec.secret), nil
	})
	if err != nil {
		return TokenMap{}, err
	}

	val, ok := token.Claims.(TokenMap)
	if !ok {
		return TokenMap{}, fmt.Errorf("corrupted token")
	}
	if !token.Valid {
		return TokenMap{}, fmt.Errorf("token is invalid")
	}

	return val, nil
}