~evanj/cms

ref: 977e0de28ca8f9d30d073665642e89fddfde493f cms/vendor/git.sr.ht/~evanj/security/security.go -rw-r--r-- 3.9 KiB
977e0de2Evan J Feat(cms.go): Retrying if requests last too long. Effective max request 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
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} }

// 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 pnic 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 val, ok := token.Claims.(TokenMap); ok && token.Valid {
		return val, nil
	}
	return TokenMap{}, err
}