auth
auth/token.go
Create a grant confirmation endpoint and its first test. Lay the framework for how we're going to write endpoints, and how we're going to test them by doing a super simple grant confirmation endpoint (where the user authorizes the grant, which can then be exchanged for a token) and a simple test to ensure that a page gets rendered when valid input is provided. We're still missing a lot of test cases: when different forms of valid input are provided (e.g., no scope, no redirect URI, etc.); when invalid input is provided; etc.
| paddy@28 | 1 package auth |
| paddy@28 | 2 |
| paddy@28 | 3 import ( |
| paddy@28 | 4 "errors" |
| paddy@28 | 5 "time" |
| paddy@28 | 6 |
| paddy@45 | 7 "code.secondbit.org/uuid" |
| paddy@28 | 8 ) |
| paddy@28 | 9 |
| paddy@28 | 10 var ( |
| paddy@49 | 11 ErrNoTokenStore = errors.New("no TokenStore was specified for the Context") |
| paddy@49 | 12 ErrTokenNotFound = errors.New("token not found in TokenStore") |
| paddy@49 | 13 ErrTokenAlreadyExists = errors.New("token already exists in TokenStore") |
| paddy@28 | 14 ) |
| paddy@28 | 15 |
| paddy@28 | 16 type Token struct { |
| paddy@28 | 17 AccessToken string |
| paddy@28 | 18 RefreshToken string |
| paddy@28 | 19 Created time.Time |
| paddy@28 | 20 ExpiresIn int32 |
| paddy@28 | 21 TokenType string |
| paddy@28 | 22 Scope string |
| paddy@28 | 23 ProfileID uuid.ID |
| paddy@28 | 24 } |
| paddy@28 | 25 |
| paddy@28 | 26 type TokenStore interface { |
| paddy@28 | 27 GetToken(token string, refresh bool) (Token, error) |
| paddy@28 | 28 SaveToken(token Token) error |
| paddy@28 | 29 RemoveToken(token string) error |
| paddy@28 | 30 GetTokensByProfileID(profileID uuid.ID, num, offset int) ([]Token, error) |
| paddy@28 | 31 } |
| paddy@28 | 32 |
| paddy@28 | 33 func (m *Memstore) GetToken(token string, refresh bool) (Token, error) { |
| paddy@28 | 34 if refresh { |
| paddy@28 | 35 t, err := m.lookupTokenByRefresh(token) |
| paddy@28 | 36 if err != nil { |
| paddy@28 | 37 return Token{}, err |
| paddy@28 | 38 } |
| paddy@28 | 39 token = t |
| paddy@28 | 40 } |
| paddy@28 | 41 m.tokenLock.RLock() |
| paddy@28 | 42 defer m.tokenLock.RUnlock() |
| paddy@28 | 43 result, ok := m.tokens[token] |
| paddy@28 | 44 if !ok { |
| paddy@28 | 45 return Token{}, ErrTokenNotFound |
| paddy@28 | 46 } |
| paddy@28 | 47 return result, nil |
| paddy@28 | 48 } |
| paddy@28 | 49 |
| paddy@28 | 50 func (m *Memstore) SaveToken(token Token) error { |
| paddy@28 | 51 m.tokenLock.Lock() |
| paddy@28 | 52 defer m.tokenLock.Unlock() |
| paddy@28 | 53 _, ok := m.tokens[token.AccessToken] |
| paddy@28 | 54 if ok { |
| paddy@28 | 55 return ErrTokenAlreadyExists |
| paddy@28 | 56 } |
| paddy@28 | 57 m.tokens[token.AccessToken] = token |
| paddy@28 | 58 if token.RefreshToken != "" { |
| paddy@28 | 59 m.refreshTokenLookup[token.RefreshToken] = token.AccessToken |
| paddy@28 | 60 } |
| paddy@28 | 61 if _, ok = m.profileTokenLookup[token.ProfileID.String()]; ok { |
| paddy@28 | 62 m.profileTokenLookup[token.ProfileID.String()] = append(m.profileTokenLookup[token.ProfileID.String()], token.AccessToken) |
| paddy@28 | 63 } else { |
| paddy@28 | 64 m.profileTokenLookup[token.ProfileID.String()] = []string{token.AccessToken} |
| paddy@28 | 65 } |
| paddy@28 | 66 return nil |
| paddy@28 | 67 } |
| paddy@28 | 68 |
| paddy@28 | 69 func (m *Memstore) RemoveToken(token string) error { |
| paddy@28 | 70 m.tokenLock.Lock() |
| paddy@28 | 71 defer m.tokenLock.Unlock() |
| paddy@28 | 72 t, ok := m.tokens[token] |
| paddy@28 | 73 if !ok { |
| paddy@28 | 74 return ErrTokenNotFound |
| paddy@28 | 75 } |
| paddy@28 | 76 delete(m.tokens, token) |
| paddy@28 | 77 if t.RefreshToken != "" { |
| paddy@28 | 78 delete(m.refreshTokenLookup, t.RefreshToken) |
| paddy@28 | 79 } |
| paddy@28 | 80 pos := -1 |
| paddy@28 | 81 for p, item := range m.profileTokenLookup[t.ProfileID.String()] { |
| paddy@28 | 82 if item == token { |
| paddy@28 | 83 pos = p |
| paddy@28 | 84 break |
| paddy@28 | 85 } |
| paddy@28 | 86 } |
| paddy@28 | 87 if pos >= 0 { |
| paddy@28 | 88 m.profileTokenLookup[t.ProfileID.String()] = append(m.profileTokenLookup[t.ProfileID.String()][:pos], m.profileTokenLookup[t.ProfileID.String()][pos+1:]...) |
| paddy@28 | 89 } |
| paddy@28 | 90 return nil |
| paddy@28 | 91 } |
| paddy@28 | 92 |
| paddy@28 | 93 func (m *Memstore) GetTokensByProfileID(profileID uuid.ID, num, offset int) ([]Token, error) { |
| paddy@28 | 94 ids, err := m.lookupTokensByProfileID(profileID.String()) |
| paddy@28 | 95 if err != nil { |
| paddy@28 | 96 return []Token{}, err |
| paddy@28 | 97 } |
| paddy@28 | 98 if len(ids) > num+offset { |
| paddy@28 | 99 ids = ids[offset : num+offset] |
| paddy@28 | 100 } else if len(ids) > offset { |
| paddy@28 | 101 ids = ids[offset:] |
| paddy@28 | 102 } else { |
| paddy@28 | 103 return []Token{}, nil |
| paddy@28 | 104 } |
| paddy@28 | 105 tokens := []Token{} |
| paddy@28 | 106 for _, id := range ids { |
| paddy@28 | 107 token, err := m.GetToken(id, false) |
| paddy@28 | 108 if err != nil { |
| paddy@28 | 109 return []Token{}, err |
| paddy@28 | 110 } |
| paddy@28 | 111 tokens = append(tokens, token) |
| paddy@28 | 112 } |
| paddy@28 | 113 return tokens, nil |
| paddy@28 | 114 } |