auth
auth/token.go
Use postgres arrays for scope associations. Use the new pqarrays library I wrote to store Scope associations for Tokens and AuthorizationCodes, instead of using our hacky and abstraction-breaking many-to-many code. We also created the authStore.deleteAuthorizationCodesByProfileID method, to clear out the AuthorizationCodes that belong to a Profile (used when the Profile is deleted). So we added the implementation for memstore and for our postgres store. Call Context.DeleteAuthorizationCodesByProfileID when deleting a Profile to clean up after it. Rename sortedScopes to Scopes, which we use pqarrays.StringArray's methods on to fulfill the sql.Scanner and driver.Valuer interfaces. This lets us store Scopes in postgres arrays. Create a stringsToScopes helper function that creates Scope objects, with their IDs filled by the strings specified. Update our GrantType.Validate function signature to return Scopes instead of []string. Create a Scopes.Strings() helper method that returns a []string of the IDs of the Scopes. Update our SQL init file to use the new postgres array definition, instead of the many-to-many definition.
| paddy@28 | 1 package auth |
| paddy@28 | 2 |
| paddy@28 | 3 import ( |
| paddy@123 | 4 "encoding/json" |
| paddy@28 | 5 "errors" |
| paddy@123 | 6 "log" |
| paddy@123 | 7 "net/http" |
| paddy@28 | 8 "time" |
| paddy@28 | 9 |
| paddy@107 | 10 "code.secondbit.org/uuid.hg" |
| paddy@28 | 11 ) |
| paddy@28 | 12 |
| paddy@69 | 13 const ( |
| paddy@125 | 14 defaultTokenExpiration = 3600 // one hour |
| paddy@69 | 15 ) |
| paddy@69 | 16 |
| paddy@123 | 17 func init() { |
| paddy@123 | 18 RegisterGrantType("refresh_token", GrantType{ |
| paddy@123 | 19 Validate: refreshTokenValidate, |
| paddy@123 | 20 Invalidate: refreshTokenInvalidate, |
| paddy@123 | 21 IssuesRefresh: true, |
| paddy@123 | 22 ReturnToken: RenderJSONToken, |
| paddy@124 | 23 AuditString: refreshTokenAuditString, |
| paddy@123 | 24 }) |
| paddy@123 | 25 } |
| paddy@123 | 26 |
| paddy@28 | 27 var ( |
| paddy@57 | 28 // ErrNoTokenStore is returned when a Context tries to act on a tokenStore without setting one first. |
| paddy@57 | 29 ErrNoTokenStore = errors.New("no tokenStore was specified for the Context") |
| paddy@57 | 30 // ErrTokenNotFound is returned when a Token is requested but not found in a tokenStore. |
| paddy@57 | 31 ErrTokenNotFound = errors.New("token not found in tokenStore") |
| paddy@57 | 32 // ErrTokenAlreadyExists is returned when a Token is added to a tokenStore, but another Token with |
| paddy@57 | 33 // the same AccessToken property already exists in the tokenStore. |
| paddy@57 | 34 ErrTokenAlreadyExists = errors.New("token already exists in tokenStore") |
| paddy@28 | 35 ) |
| paddy@28 | 36 |
| paddy@57 | 37 // Token represents an access and/or refresh token that the Client can use to access user data |
| paddy@57 | 38 // or obtain a new access token. |
| paddy@28 | 39 type Token struct { |
| paddy@125 | 40 AccessToken string |
| paddy@125 | 41 RefreshToken string |
| paddy@125 | 42 Created time.Time |
| paddy@125 | 43 CreatedFrom string |
| paddy@125 | 44 ExpiresIn int32 |
| paddy@125 | 45 TokenType string |
| paddy@163 | 46 Scopes Scopes |
| paddy@125 | 47 ProfileID uuid.ID |
| paddy@125 | 48 ClientID uuid.ID |
| paddy@125 | 49 Revoked bool |
| paddy@125 | 50 RefreshRevoked bool |
| paddy@28 | 51 } |
| paddy@28 | 52 |
| paddy@57 | 53 type tokenStore interface { |
| paddy@57 | 54 getToken(token string, refresh bool) (Token, error) |
| paddy@57 | 55 saveToken(token Token) error |
| paddy@91 | 56 revokeToken(token string, refresh bool) error |
| paddy@57 | 57 getTokensByProfileID(profileID uuid.ID, num, offset int) ([]Token, error) |
| paddy@162 | 58 revokeTokensByProfileID(profileID uuid.ID) error |
| paddy@28 | 59 } |
| paddy@28 | 60 |
| paddy@57 | 61 func (m *memstore) getToken(token string, refresh bool) (Token, error) { |
| paddy@28 | 62 if refresh { |
| paddy@28 | 63 t, err := m.lookupTokenByRefresh(token) |
| paddy@28 | 64 if err != nil { |
| paddy@28 | 65 return Token{}, err |
| paddy@28 | 66 } |
| paddy@28 | 67 token = t |
| paddy@28 | 68 } |
| paddy@28 | 69 m.tokenLock.RLock() |
| paddy@28 | 70 defer m.tokenLock.RUnlock() |
| paddy@28 | 71 result, ok := m.tokens[token] |
| paddy@28 | 72 if !ok { |
| paddy@28 | 73 return Token{}, ErrTokenNotFound |
| paddy@28 | 74 } |
| paddy@28 | 75 return result, nil |
| paddy@28 | 76 } |
| paddy@28 | 77 |
| paddy@57 | 78 func (m *memstore) saveToken(token Token) error { |
| paddy@28 | 79 m.tokenLock.Lock() |
| paddy@28 | 80 defer m.tokenLock.Unlock() |
| paddy@28 | 81 _, ok := m.tokens[token.AccessToken] |
| paddy@28 | 82 if ok { |
| paddy@28 | 83 return ErrTokenAlreadyExists |
| paddy@28 | 84 } |
| paddy@28 | 85 m.tokens[token.AccessToken] = token |
| paddy@28 | 86 if token.RefreshToken != "" { |
| paddy@28 | 87 m.refreshTokenLookup[token.RefreshToken] = token.AccessToken |
| paddy@28 | 88 } |
| paddy@28 | 89 if _, ok = m.profileTokenLookup[token.ProfileID.String()]; ok { |
| paddy@28 | 90 m.profileTokenLookup[token.ProfileID.String()] = append(m.profileTokenLookup[token.ProfileID.String()], token.AccessToken) |
| paddy@28 | 91 } else { |
| paddy@28 | 92 m.profileTokenLookup[token.ProfileID.String()] = []string{token.AccessToken} |
| paddy@28 | 93 } |
| paddy@28 | 94 return nil |
| paddy@28 | 95 } |
| paddy@28 | 96 |
| paddy@91 | 97 func (m *memstore) revokeToken(token string, refresh bool) error { |
| paddy@91 | 98 if refresh { |
| paddy@91 | 99 t, err := m.lookupTokenByRefresh(token) |
| paddy@91 | 100 if err != nil { |
| paddy@91 | 101 return err |
| paddy@91 | 102 } |
| paddy@91 | 103 token = t |
| paddy@91 | 104 } |
| paddy@91 | 105 m.tokenLock.Lock() |
| paddy@91 | 106 defer m.tokenLock.Unlock() |
| paddy@91 | 107 t, ok := m.tokens[token] |
| paddy@91 | 108 if !ok { |
| paddy@91 | 109 return ErrTokenNotFound |
| paddy@91 | 110 } |
| paddy@123 | 111 if refresh { |
| paddy@123 | 112 t.RefreshRevoked = true |
| paddy@123 | 113 } else { |
| paddy@123 | 114 t.Revoked = true |
| paddy@123 | 115 } |
| paddy@91 | 116 m.tokens[token] = t |
| paddy@91 | 117 return nil |
| paddy@91 | 118 } |
| paddy@91 | 119 |
| paddy@162 | 120 func (m *memstore) revokeTokensByProfileID(profileID uuid.ID) error { |
| paddy@162 | 121 ids, err := m.lookupTokensByProfileID(profileID.String()) |
| paddy@162 | 122 if err != nil { |
| paddy@162 | 123 return err |
| paddy@162 | 124 } |
| paddy@162 | 125 if len(ids) < 1 { |
| paddy@162 | 126 return ErrProfileNotFound |
| paddy@162 | 127 } |
| paddy@162 | 128 m.tokenLock.Lock() |
| paddy@162 | 129 defer m.tokenLock.Unlock() |
| paddy@162 | 130 for _, id := range ids { |
| paddy@162 | 131 delete(m.tokens, id) |
| paddy@162 | 132 } |
| paddy@162 | 133 return nil |
| paddy@162 | 134 } |
| paddy@162 | 135 |
| paddy@57 | 136 func (m *memstore) getTokensByProfileID(profileID uuid.ID, num, offset int) ([]Token, error) { |
| paddy@28 | 137 ids, err := m.lookupTokensByProfileID(profileID.String()) |
| paddy@28 | 138 if err != nil { |
| paddy@28 | 139 return []Token{}, err |
| paddy@28 | 140 } |
| paddy@28 | 141 if len(ids) > num+offset { |
| paddy@28 | 142 ids = ids[offset : num+offset] |
| paddy@28 | 143 } else if len(ids) > offset { |
| paddy@28 | 144 ids = ids[offset:] |
| paddy@28 | 145 } else { |
| paddy@28 | 146 return []Token{}, nil |
| paddy@28 | 147 } |
| paddy@28 | 148 tokens := []Token{} |
| paddy@28 | 149 for _, id := range ids { |
| paddy@57 | 150 token, err := m.getToken(id, false) |
| paddy@28 | 151 if err != nil { |
| paddy@28 | 152 return []Token{}, err |
| paddy@28 | 153 } |
| paddy@28 | 154 tokens = append(tokens, token) |
| paddy@28 | 155 } |
| paddy@28 | 156 return tokens, nil |
| paddy@28 | 157 } |
| paddy@123 | 158 |
| paddy@163 | 159 func refreshTokenValidate(w http.ResponseWriter, r *http.Request, context Context) (scopes Scopes, profileID uuid.ID, valid bool) { |
| paddy@123 | 160 enc := json.NewEncoder(w) |
| paddy@123 | 161 refresh := r.PostFormValue("refresh_token") |
| paddy@123 | 162 if refresh == "" { |
| paddy@123 | 163 w.WriteHeader(http.StatusBadRequest) |
| paddy@123 | 164 renderJSONError(enc, "invalid_request") |
| paddy@123 | 165 return |
| paddy@123 | 166 } |
| paddy@123 | 167 token, err := context.GetToken(refresh, true) |
| paddy@123 | 168 if err != nil { |
| paddy@123 | 169 if err == ErrTokenNotFound { |
| paddy@123 | 170 w.WriteHeader(http.StatusBadRequest) |
| paddy@123 | 171 renderJSONError(enc, "invalid_grant") |
| paddy@123 | 172 return |
| paddy@123 | 173 } |
| paddy@123 | 174 log.Println("Error exchanging refresh token:", err) |
| paddy@123 | 175 w.WriteHeader(http.StatusInternalServerError) |
| paddy@123 | 176 renderJSONError(enc, "server_error") |
| paddy@123 | 177 return |
| paddy@123 | 178 } |
| paddy@123 | 179 clientID, _, ok := getClientAuth(w, r, true) |
| paddy@123 | 180 if !ok { |
| paddy@123 | 181 return |
| paddy@123 | 182 } |
| paddy@123 | 183 if !token.ClientID.Equal(clientID) { |
| paddy@123 | 184 w.WriteHeader(http.StatusBadRequest) |
| paddy@123 | 185 renderJSONError(enc, "invalid_grant") |
| paddy@123 | 186 return |
| paddy@123 | 187 } |
| paddy@123 | 188 if token.RefreshRevoked { |
| paddy@123 | 189 w.WriteHeader(http.StatusBadRequest) |
| paddy@123 | 190 renderJSONError(enc, "invalid_grant") |
| paddy@123 | 191 return |
| paddy@123 | 192 } |
| paddy@135 | 193 return token.Scopes, token.ProfileID, true |
| paddy@123 | 194 } |
| paddy@123 | 195 |
| paddy@123 | 196 func refreshTokenInvalidate(r *http.Request, context Context) error { |
| paddy@123 | 197 refresh := r.PostFormValue("refresh_token") |
| paddy@123 | 198 if refresh == "" { |
| paddy@123 | 199 return ErrTokenNotFound |
| paddy@123 | 200 } |
| paddy@123 | 201 return context.RevokeToken(refresh, true) |
| paddy@123 | 202 } |
| paddy@124 | 203 |
| paddy@124 | 204 func refreshTokenAuditString(r *http.Request) string { |
| paddy@124 | 205 return "refresh_token:" + r.PostFormValue("refresh_token") |
| paddy@124 | 206 } |
| paddy@128 | 207 |
| paddy@128 | 208 // BUG(paddy): We need to implement a handler for revoking a token. |
| paddy@128 | 209 // BUG(paddy): We need to implement a handler for listing active tokens. |