11package auth
22
33import (
4+ "crypto/rand"
5+ "encoding/base64"
46 "encoding/json"
57 "errors"
68 "fmt"
@@ -23,6 +25,9 @@ const (
2325
2426 headerKey = "Authorization"
2527 headerValStart = "Bearer "
28+
29+ // number of bytes to use when generating a new random token ID
30+ tokenIDByteLen = 20
2631)
2732
2833var (
@@ -36,23 +41,37 @@ var (
3641 headerKey ,
3742 headerValStart ,
3843 )
39- ErrTokenExpired = errors . New ( "the provided auth token was expired " )
44+ ErrInvalidSigningMethod = fmt . Errorf ( " auth token didn't specify the HS256 signing method correctly " )
4045 ErrTokenRevoked = errors .New ("the provided auth token was revoked" )
4146 ErrTokenInsufficientPermission = errors .New ("the provided auth token does not allow access to this endpoint" )
4247
43- errWrongPassword = errors .New ("incorrect password" )
44- errInvalidTokenFormat = errors .New ("token is invalid format" )
45- errSamePassword = errors .New ("new password can't be same as old password" )
48+ errWrongPassword = errors .New ("incorrect password" )
49+ errSamePassword = errors .New ("new password can't be same as old password" )
4650)
4751
4852// Auth handles HTTP API authorization for this node
4953type Auth struct {
50- Enabled bool // True iff API calls need auth token
51- Password password.Hash // Hash of the password. Can be changed via API call.
54+ lock sync.RWMutex // Prevent race condition when accessing password
55+ enabled bool // True iff API calls need auth token
56+ password password.Hash // Hash of the password. Can be changed via API call.
57+ clock timer.Clock // Tells the time. Can be faked for testing
58+ revoked map [string ]struct {} // Set of token IDs that have been revoked
59+ }
5260
53- lock sync.RWMutex // Prevent race condition when accessing password
54- clock timer.Clock // Tells the time. Can be faked for testing
55- revoked []string // List of tokens that have been revoked
61+ func New (enabled bool , password string ) (* Auth , error ) {
62+ auth := & Auth {
63+ enabled : enabled ,
64+ revoked : make (map [string ]struct {}),
65+ }
66+ return auth , auth .password .Set (password )
67+ }
68+
69+ func NewFromHash (enabled bool , password password.Hash ) * Auth {
70+ return & Auth {
71+ enabled : enabled ,
72+ password : password ,
73+ revoked : make (map [string ]struct {}),
74+ }
5675}
5776
5877// Custom claim type used for API access token
@@ -62,26 +81,15 @@ type endpointClaims struct {
6281 // Each element is an endpoint that the token allows access to
6382 // If endpoints has an element "*", allows access to all API endpoints
6483 // In this case, "*" should be the only element of [endpoints]
65- Endpoints []string
84+ Endpoints []string `json:"endpoints,omitempty"`
6685}
6786
6887// getTokenKey returns the key to use when making and parsing tokens
69- func (auth * Auth ) getTokenKey (* jwt.Token ) (interface {}, error ) {
70- return auth .Password .Password [:], nil
71- }
72-
73- // getToken gets the JWT token from the request header
74- // Assumes the header is this form:
75- // "Authorization": "Bearer TOKEN.GOES.HERE"
76- func getToken (r * http.Request ) (string , error ) {
77- rawHeader := r .Header .Get (headerKey ) // Should be "Bearer AUTH.TOKEN.HERE"
78- if rawHeader == "" {
79- return "" , ErrNoToken
80- }
81- if ! strings .HasPrefix (rawHeader , headerValStart ) {
82- return "" , errInvalidTokenFormat
88+ func (auth * Auth ) getTokenKey (t * jwt.Token ) (interface {}, error ) {
89+ if t .Method != jwt .SigningMethodHS256 {
90+ return nil , ErrInvalidSigningMethod
8391 }
84- return rawHeader [ len ( headerValStart ) :], nil // Returns actual auth token. Slice guaranteed to not go OOB
92+ return auth . password . Password [ :], nil
8593}
8694
8795// Create and return a new token that allows access to each API endpoint such
@@ -90,29 +98,38 @@ func getToken(r *http.Request) (string, error) {
9098func (auth * Auth ) newToken (password string , endpoints []string ) (string , error ) {
9199 auth .lock .RLock ()
92100 defer auth .lock .RUnlock ()
93- if ! auth .Password .Check (password ) {
101+
102+ if ! auth .password .Check (password ) {
94103 return "" , errWrongPassword
95104 }
105+
96106 canAccessAll := false
97107 for _ , endpoint := range endpoints {
98108 if endpoint == "*" {
99109 canAccessAll = true
100110 break
101111 }
102112 }
113+
114+ idBytes := [tokenIDByteLen ]byte {}
115+ if _ , err := rand .Read (idBytes [:]); err != nil {
116+ return "" , fmt .Errorf ("failed to generate the unique token ID due to %w" , err )
117+ }
118+ id := base64 .URLEncoding .EncodeToString (idBytes [:])
119+
103120 claims := endpointClaims {
104121 StandardClaims : jwt.StandardClaims {
105122 ExpiresAt : auth .clock .Time ().Add (TokenLifespan ).Unix (),
123+ Id : id ,
106124 },
107125 }
108126 if canAccessAll {
109127 claims .Endpoints = []string {"*" }
110128 } else {
111129 claims .Endpoints = endpoints
112130 }
113- token := jwt .NewWithClaims (jwt .SigningMethodHS256 , claims )
114- return token .SignedString (auth .Password .Password [:]) // Sign the token and return its string repr.
115-
131+ token := jwt .NewWithClaims (jwt .SigningMethodHS256 , & claims )
132+ return token .SignedString (auth .password .Password [:]) // Sign the token and return its string repr.
116133}
117134
118135// Revokes the token whose string repr. is [tokenStr]; it will not be accepted as authorization for future API calls.
@@ -124,23 +141,61 @@ func (auth *Auth) newToken(password string, endpoints []string) (string, error)
124141func (auth * Auth ) revokeToken (tokenStr string , password string ) error {
125142 auth .lock .Lock ()
126143 defer auth .lock .Unlock ()
127- if ! auth .Password .Check (password ) {
144+
145+ if ! auth .password .Check (password ) {
128146 return errWrongPassword
129147 }
130148
131149 // See if token is well-formed and signature is right
132- token , err := jwt .Parse (tokenStr , auth .getTokenKey )
150+ token , err := jwt .ParseWithClaims (tokenStr , & endpointClaims {} , auth .getTokenKey )
133151 if err != nil {
134152 return err
135153 }
136154
137- // Only need to revoke if the token is valid
138- if token .Valid {
139- auth .revoked = append (auth .revoked , tokenStr )
155+ // If the token isn't valid, it has essentially already been revoked.
156+ if ! token .Valid {
157+ return nil
158+ }
159+
160+ claims , ok := token .Claims .(* endpointClaims )
161+ if ! ok {
162+ return fmt .Errorf ("expected auth token's claims to be type endpointClaims but is %T" , token .Claims )
140163 }
164+ auth .revoked [claims .Id ] = struct {}{}
141165 return nil
142166}
143167
168+ // Authenticates [tokenStr] for access to [url].
169+ func (auth * Auth ) authenticateToken (tokenStr , url string ) error {
170+ auth .lock .RLock ()
171+ defer auth .lock .RUnlock ()
172+
173+ token , err := jwt .ParseWithClaims (tokenStr , & endpointClaims {}, auth .getTokenKey )
174+ if err != nil { // Probably because signature wrong
175+ return err
176+ }
177+
178+ // Make sure this token gives access to the requested endpoint
179+ claims , ok := token .Claims .(* endpointClaims )
180+ if ! ok {
181+ // Error is intentionally dropped here as there is nothing left to do
182+ // with it.
183+ return fmt .Errorf ("expected auth token's claims to be type endpointClaims but is %T" , token .Claims )
184+ }
185+
186+ _ , revoked := auth .revoked [claims .Id ]
187+ if revoked {
188+ return ErrTokenRevoked
189+ }
190+
191+ for _ , endpoint := range claims .Endpoints {
192+ if endpoint == "*" || strings .HasSuffix (url , endpoint ) {
193+ return nil
194+ }
195+ }
196+ return ErrTokenInsufficientPermission
197+ }
198+
144199// Change the password required to create and revoke tokens.
145200// [oldPassword] is the current password.
146201// [newPassword] is the new password. It can't be the empty string and it can't
@@ -154,100 +209,56 @@ func (auth *Auth) changePassword(oldPassword, newPassword string) error {
154209 auth .lock .Lock ()
155210 defer auth .lock .Unlock ()
156211
157- if ! auth .Password .Check (oldPassword ) {
212+ if ! auth .password .Check (oldPassword ) {
158213 return errWrongPassword
159214 }
160215 if err := password .IsValid (newPassword , password .OK ); err != nil {
161216 return err
162217 }
163- if err := auth .Password .Set (newPassword ); err != nil {
218+ if err := auth .password .Set (newPassword ); err != nil {
164219 return err
165220 }
166221
167222 // All the revoked tokens are now invalid; no need to mark specifically as
168223 // revoked.
169- auth .revoked = nil
224+ auth .revoked = make ( map [ string ] struct {})
170225 return nil
171226}
172227
173228// WrapHandler wraps a handler. Before passing a request to the handler, check that
174229// an auth token was provided (if necessary) and that it is valid/unexpired.
175230func (auth * Auth ) WrapHandler (h http.Handler ) http.Handler {
176- if ! auth .Enabled { // Auth tokens aren't in use. Do nothing.
231+ if ! auth .enabled { // Auth tokens aren't in use. Do nothing.
177232 return h
178233 }
179234 return http .HandlerFunc (func (w http.ResponseWriter , r * http.Request ) {
180- if path .Base (r .URL .Path ) == Endpoint { // Don't require auth token to hit auth endpoint
235+ // Don't require auth token to hit auth endpoint
236+ if path .Base (r .URL .Path ) == Endpoint {
181237 h .ServeHTTP (w , r )
182238 return
183239 }
184240
185- tokenStr , err := getToken (r ) // Get the token from the header
186- if err == ErrNoToken {
187- // Error is intentionally dropped here as there is nothing left to
188- // do with it.
189- writeUnauthorizedResponse (w , err )
190- return
191- } else if err != nil {
192- // Error is intentionally dropped here as there is nothing left to
193- // do with it.
194- writeUnauthorizedResponse (w , ErrAuthHeaderNotParsable )
241+ // Should be "Bearer AUTH.TOKEN.HERE"
242+ rawHeader := r .Header .Get (headerKey )
243+ if rawHeader == "" {
244+ writeUnauthorizedResponse (w , ErrNoToken )
195245 return
196246 }
197-
198- auth .lock .RLock ()
199- token , err := jwt .ParseWithClaims (tokenStr , & endpointClaims {}, auth .getTokenKey )
200- auth .lock .RUnlock ()
201-
202- if err != nil { // Probably because signature wrong
247+ if ! strings .HasPrefix (rawHeader , headerValStart ) {
203248 // Error is intentionally dropped here as there is nothing left to
204249 // do with it.
205-
206- if strings .Contains (err .Error (), "expired" ) {
207- writeUnauthorizedResponse (w , ErrTokenExpired )
208- return
209- }
210-
211- writeUnauthorizedResponse (w , fmt .Errorf ("invalid auth token: %s" , err ))
250+ writeUnauthorizedResponse (w , ErrAuthHeaderNotParsable )
212251 return
213252 }
253+ // Returns actual auth token. Slice guaranteed to not go OOB
254+ tokenStr := rawHeader [len (headerValStart ):]
214255
215- // Make sure this token gives access to the requested endpoint
216- claims , ok := token .Claims .(* endpointClaims )
217- if ! ok {
218- // Error is intentionally dropped here as there is nothing left to
219- // do with it.
220- err := fmt .Errorf ("expected auth token's claims to be type endpointClaims but is %T" , token .Claims )
256+ if err := auth .authenticateToken (tokenStr , r .URL .Path ); err != nil {
221257 writeUnauthorizedResponse (w , err )
222258 return
223259 }
224- canAccess := false // true iff the token authorizes access to the API
225- for _ , endpoint := range claims .Endpoints {
226- if endpoint == "*" || strings .HasSuffix (r .URL .Path , endpoint ) {
227- canAccess = true
228- break
229- }
230- }
231- if ! canAccess {
232- // Error is intentionally dropped here as there is nothing left to
233- // do with it.
234- writeUnauthorizedResponse (w , ErrTokenInsufficientPermission )
235- return
236- }
237-
238- auth .lock .RLock ()
239- for _ , revokedToken := range auth .revoked { // Make sure this token wasn't revoked
240- if revokedToken == tokenStr {
241- // Error is intentionally dropped here as there is nothing left
242- // to do with it.
243- writeUnauthorizedResponse (w , ErrTokenRevoked )
244- auth .lock .RUnlock ()
245- return
246- }
247- }
248- auth .lock .RUnlock ()
249260
250- h .ServeHTTP (w , r ) // Authorization successful
261+ h .ServeHTTP (w , r )
251262 })
252263}
253264
0 commit comments