auth
auth/token.go
Fill out token.CreatedFrom. Add a GrantType.AuditString() string method that will return a string for an audit log. Basically, it returns enough information to identify how the token got created. For client credentials, that's just the string "client_credentials". For user credentials, that's just the string "credentials". For auth codes, that's "authcode:", followed by the code used. For refresh tokens, that's "refresh_token:", followed by the refresh token used.
| 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@88 | 14 defaultTokenExpiration = 3600 // one hour |
| paddy@88 | 15 defaultRefreshTokenExpiration = 86400 // one day |
| paddy@69 | 16 ) |
| paddy@69 | 17 |
| paddy@123 | 18 func init() { |
| paddy@123 | 19 RegisterGrantType("refresh_token", GrantType{ |
| paddy@123 | 20 Validate: refreshTokenValidate, |
| paddy@123 | 21 Invalidate: refreshTokenInvalidate, |
| paddy@123 | 22 IssuesRefresh: true, |
| paddy@123 | 23 ReturnToken: RenderJSONToken, |
| paddy@124 | 24 AuditString: refreshTokenAuditString, |
| paddy@123 | 25 }) |
| paddy@123 | 26 } |
| paddy@123 | 27 |
| paddy@28 | 28 var ( |
| paddy@57 | 29 // ErrNoTokenStore is returned when a Context tries to act on a tokenStore without setting one first. |
| paddy@57 | 30 ErrNoTokenStore = errors.New("no tokenStore was specified for the Context") |
| paddy@57 | 31 // ErrTokenNotFound is returned when a Token is requested but not found in a tokenStore. |
| paddy@57 | 32 ErrTokenNotFound = errors.New("token not found in tokenStore") |
| paddy@57 | 33 // ErrTokenAlreadyExists is returned when a Token is added to a tokenStore, but another Token with |
| paddy@57 | 34 // the same AccessToken property already exists in the tokenStore. |
| paddy@57 | 35 ErrTokenAlreadyExists = errors.New("token already exists in tokenStore") |
| paddy@28 | 36 ) |
| paddy@28 | 37 |
| paddy@57 | 38 // Token represents an access and/or refresh token that the Client can use to access user data |
| paddy@57 | 39 // or obtain a new access token. |
| paddy@28 | 40 type Token struct { |
| paddy@88 | 41 AccessToken string |
| paddy@88 | 42 RefreshToken string |
| paddy@88 | 43 Created time.Time |
| paddy@88 | 44 CreatedFrom string |
| paddy@88 | 45 ExpiresIn int32 |
| paddy@88 | 46 RefreshExpiresIn int32 |
| paddy@88 | 47 TokenType string |
| paddy@88 | 48 Scope string |
| paddy@88 | 49 ProfileID uuid.ID |
| paddy@123 | 50 ClientID uuid.ID |
| paddy@88 | 51 Revoked bool |
| paddy@123 | 52 RefreshRevoked bool |
| paddy@28 | 53 } |
| paddy@28 | 54 |
| paddy@57 | 55 type tokenStore interface { |
| paddy@57 | 56 getToken(token string, refresh bool) (Token, error) |
| paddy@57 | 57 saveToken(token Token) error |
| paddy@57 | 58 removeToken(token string) error |
| paddy@91 | 59 revokeToken(token string, refresh bool) error |
| paddy@57 | 60 getTokensByProfileID(profileID uuid.ID, num, offset int) ([]Token, error) |
| paddy@28 | 61 } |
| paddy@28 | 62 |
| paddy@57 | 63 func (m *memstore) getToken(token string, refresh bool) (Token, error) { |
| paddy@28 | 64 if refresh { |
| paddy@28 | 65 t, err := m.lookupTokenByRefresh(token) |
| paddy@28 | 66 if err != nil { |
| paddy@28 | 67 return Token{}, err |
| paddy@28 | 68 } |
| paddy@28 | 69 token = t |
| paddy@28 | 70 } |
| paddy@28 | 71 m.tokenLock.RLock() |
| paddy@28 | 72 defer m.tokenLock.RUnlock() |
| paddy@28 | 73 result, ok := m.tokens[token] |
| paddy@28 | 74 if !ok { |
| paddy@28 | 75 return Token{}, ErrTokenNotFound |
| paddy@28 | 76 } |
| paddy@28 | 77 return result, nil |
| paddy@28 | 78 } |
| paddy@28 | 79 |
| paddy@57 | 80 func (m *memstore) saveToken(token Token) error { |
| paddy@28 | 81 m.tokenLock.Lock() |
| paddy@28 | 82 defer m.tokenLock.Unlock() |
| paddy@28 | 83 _, ok := m.tokens[token.AccessToken] |
| paddy@28 | 84 if ok { |
| paddy@28 | 85 return ErrTokenAlreadyExists |
| paddy@28 | 86 } |
| paddy@28 | 87 m.tokens[token.AccessToken] = token |
| paddy@28 | 88 if token.RefreshToken != "" { |
| paddy@28 | 89 m.refreshTokenLookup[token.RefreshToken] = token.AccessToken |
| paddy@28 | 90 } |
| paddy@28 | 91 if _, ok = m.profileTokenLookup[token.ProfileID.String()]; ok { |
| paddy@28 | 92 m.profileTokenLookup[token.ProfileID.String()] = append(m.profileTokenLookup[token.ProfileID.String()], token.AccessToken) |
| paddy@28 | 93 } else { |
| paddy@28 | 94 m.profileTokenLookup[token.ProfileID.String()] = []string{token.AccessToken} |
| paddy@28 | 95 } |
| paddy@28 | 96 return nil |
| paddy@28 | 97 } |
| paddy@28 | 98 |
| paddy@57 | 99 func (m *memstore) removeToken(token string) error { |
| paddy@28 | 100 m.tokenLock.Lock() |
| paddy@28 | 101 defer m.tokenLock.Unlock() |
| paddy@28 | 102 t, ok := m.tokens[token] |
| paddy@28 | 103 if !ok { |
| paddy@28 | 104 return ErrTokenNotFound |
| paddy@28 | 105 } |
| paddy@28 | 106 delete(m.tokens, token) |
| paddy@28 | 107 if t.RefreshToken != "" { |
| paddy@28 | 108 delete(m.refreshTokenLookup, t.RefreshToken) |
| paddy@28 | 109 } |
| paddy@28 | 110 pos := -1 |
| paddy@28 | 111 for p, item := range m.profileTokenLookup[t.ProfileID.String()] { |
| paddy@28 | 112 if item == token { |
| paddy@28 | 113 pos = p |
| paddy@28 | 114 break |
| paddy@28 | 115 } |
| paddy@28 | 116 } |
| paddy@28 | 117 if pos >= 0 { |
| paddy@28 | 118 m.profileTokenLookup[t.ProfileID.String()] = append(m.profileTokenLookup[t.ProfileID.String()][:pos], m.profileTokenLookup[t.ProfileID.String()][pos+1:]...) |
| paddy@28 | 119 } |
| paddy@28 | 120 return nil |
| paddy@28 | 121 } |
| paddy@28 | 122 |
| paddy@91 | 123 func (m *memstore) revokeToken(token string, refresh bool) error { |
| paddy@91 | 124 if refresh { |
| paddy@91 | 125 t, err := m.lookupTokenByRefresh(token) |
| paddy@91 | 126 if err != nil { |
| paddy@91 | 127 return err |
| paddy@91 | 128 } |
| paddy@91 | 129 token = t |
| paddy@91 | 130 } |
| paddy@91 | 131 m.tokenLock.Lock() |
| paddy@91 | 132 defer m.tokenLock.Unlock() |
| paddy@91 | 133 t, ok := m.tokens[token] |
| paddy@91 | 134 if !ok { |
| paddy@91 | 135 return ErrTokenNotFound |
| paddy@91 | 136 } |
| paddy@123 | 137 if refresh { |
| paddy@123 | 138 t.RefreshRevoked = true |
| paddy@123 | 139 } else { |
| paddy@123 | 140 t.Revoked = true |
| paddy@123 | 141 } |
| paddy@91 | 142 m.tokens[token] = t |
| paddy@91 | 143 return nil |
| paddy@91 | 144 } |
| paddy@91 | 145 |
| paddy@57 | 146 func (m *memstore) getTokensByProfileID(profileID uuid.ID, num, offset int) ([]Token, error) { |
| paddy@28 | 147 ids, err := m.lookupTokensByProfileID(profileID.String()) |
| paddy@28 | 148 if err != nil { |
| paddy@28 | 149 return []Token{}, err |
| paddy@28 | 150 } |
| paddy@28 | 151 if len(ids) > num+offset { |
| paddy@28 | 152 ids = ids[offset : num+offset] |
| paddy@28 | 153 } else if len(ids) > offset { |
| paddy@28 | 154 ids = ids[offset:] |
| paddy@28 | 155 } else { |
| paddy@28 | 156 return []Token{}, nil |
| paddy@28 | 157 } |
| paddy@28 | 158 tokens := []Token{} |
| paddy@28 | 159 for _, id := range ids { |
| paddy@57 | 160 token, err := m.getToken(id, false) |
| paddy@28 | 161 if err != nil { |
| paddy@28 | 162 return []Token{}, err |
| paddy@28 | 163 } |
| paddy@28 | 164 tokens = append(tokens, token) |
| paddy@28 | 165 } |
| paddy@28 | 166 return tokens, nil |
| paddy@28 | 167 } |
| paddy@123 | 168 |
| paddy@123 | 169 func refreshTokenValidate(w http.ResponseWriter, r *http.Request, context Context) (scope string, profileID uuid.ID, valid bool) { |
| paddy@123 | 170 enc := json.NewEncoder(w) |
| paddy@123 | 171 refresh := r.PostFormValue("refresh_token") |
| paddy@123 | 172 if refresh == "" { |
| paddy@123 | 173 w.WriteHeader(http.StatusBadRequest) |
| paddy@123 | 174 renderJSONError(enc, "invalid_request") |
| paddy@123 | 175 return |
| paddy@123 | 176 } |
| paddy@123 | 177 token, err := context.GetToken(refresh, true) |
| paddy@123 | 178 if err != nil { |
| paddy@123 | 179 if err == ErrTokenNotFound { |
| paddy@123 | 180 w.WriteHeader(http.StatusBadRequest) |
| paddy@123 | 181 renderJSONError(enc, "invalid_grant") |
| paddy@123 | 182 return |
| paddy@123 | 183 } |
| paddy@123 | 184 log.Println("Error exchanging refresh token:", err) |
| paddy@123 | 185 w.WriteHeader(http.StatusInternalServerError) |
| paddy@123 | 186 renderJSONError(enc, "server_error") |
| paddy@123 | 187 return |
| paddy@123 | 188 } |
| paddy@123 | 189 clientID, _, ok := getClientAuth(w, r, true) |
| paddy@123 | 190 if !ok { |
| paddy@123 | 191 return |
| paddy@123 | 192 } |
| paddy@123 | 193 if !token.ClientID.Equal(clientID) { |
| paddy@123 | 194 w.WriteHeader(http.StatusBadRequest) |
| paddy@123 | 195 renderJSONError(enc, "invalid_grant") |
| paddy@123 | 196 return |
| paddy@123 | 197 } |
| paddy@123 | 198 if token.RefreshRevoked { |
| paddy@123 | 199 w.WriteHeader(http.StatusBadRequest) |
| paddy@123 | 200 renderJSONError(enc, "invalid_grant") |
| paddy@123 | 201 return |
| paddy@123 | 202 } |
| paddy@123 | 203 expires := token.Created.Add(time.Duration(token.RefreshExpiresIn) * time.Second) |
| paddy@123 | 204 if expires.Before(time.Now()) { |
| paddy@123 | 205 w.WriteHeader(http.StatusBadRequest) |
| paddy@123 | 206 renderJSONError(enc, "invalid_grant") |
| paddy@123 | 207 return |
| paddy@123 | 208 } |
| paddy@123 | 209 return token.Scope, token.ProfileID, true |
| paddy@123 | 210 } |
| paddy@123 | 211 |
| paddy@123 | 212 func refreshTokenInvalidate(r *http.Request, context Context) error { |
| paddy@123 | 213 refresh := r.PostFormValue("refresh_token") |
| paddy@123 | 214 if refresh == "" { |
| paddy@123 | 215 return ErrTokenNotFound |
| paddy@123 | 216 } |
| paddy@123 | 217 return context.RevokeToken(refresh, true) |
| paddy@123 | 218 } |
| paddy@124 | 219 |
| paddy@124 | 220 func refreshTokenAuditString(r *http.Request) string { |
| paddy@124 | 221 return "refresh_token:" + r.PostFormValue("refresh_token") |
| paddy@124 | 222 } |