auth

Paddy 2015-04-11 Parent:762953f6a7f2 Child:6f473576c6ae

157:202e991accc2 Go to Latest

auth/token.go

Wire up the postgres database for authd. Have authd use the AUTH_PG_DB environment variable to detect support for the postgres *Stores, and if postgres is supported, use it. If postgres isn't supported, fall back on the in-memory store. Also create-if-not-exists the test scopes, instead of panicking when the scope already exists.

History
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@155 46 Scopes []string `sql_column:"-"`
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@28 58 }
paddy@28 59
paddy@57 60 func (m *memstore) getToken(token string, refresh bool) (Token, error) {
paddy@28 61 if refresh {
paddy@28 62 t, err := m.lookupTokenByRefresh(token)
paddy@28 63 if err != nil {
paddy@28 64 return Token{}, err
paddy@28 65 }
paddy@28 66 token = t
paddy@28 67 }
paddy@28 68 m.tokenLock.RLock()
paddy@28 69 defer m.tokenLock.RUnlock()
paddy@28 70 result, ok := m.tokens[token]
paddy@28 71 if !ok {
paddy@28 72 return Token{}, ErrTokenNotFound
paddy@28 73 }
paddy@28 74 return result, nil
paddy@28 75 }
paddy@28 76
paddy@57 77 func (m *memstore) saveToken(token Token) error {
paddy@28 78 m.tokenLock.Lock()
paddy@28 79 defer m.tokenLock.Unlock()
paddy@28 80 _, ok := m.tokens[token.AccessToken]
paddy@28 81 if ok {
paddy@28 82 return ErrTokenAlreadyExists
paddy@28 83 }
paddy@28 84 m.tokens[token.AccessToken] = token
paddy@28 85 if token.RefreshToken != "" {
paddy@28 86 m.refreshTokenLookup[token.RefreshToken] = token.AccessToken
paddy@28 87 }
paddy@28 88 if _, ok = m.profileTokenLookup[token.ProfileID.String()]; ok {
paddy@28 89 m.profileTokenLookup[token.ProfileID.String()] = append(m.profileTokenLookup[token.ProfileID.String()], token.AccessToken)
paddy@28 90 } else {
paddy@28 91 m.profileTokenLookup[token.ProfileID.String()] = []string{token.AccessToken}
paddy@28 92 }
paddy@28 93 return nil
paddy@28 94 }
paddy@28 95
paddy@91 96 func (m *memstore) revokeToken(token string, refresh bool) error {
paddy@91 97 if refresh {
paddy@91 98 t, err := m.lookupTokenByRefresh(token)
paddy@91 99 if err != nil {
paddy@91 100 return err
paddy@91 101 }
paddy@91 102 token = t
paddy@91 103 }
paddy@91 104 m.tokenLock.Lock()
paddy@91 105 defer m.tokenLock.Unlock()
paddy@91 106 t, ok := m.tokens[token]
paddy@91 107 if !ok {
paddy@91 108 return ErrTokenNotFound
paddy@91 109 }
paddy@123 110 if refresh {
paddy@123 111 t.RefreshRevoked = true
paddy@123 112 } else {
paddy@123 113 t.Revoked = true
paddy@123 114 }
paddy@91 115 m.tokens[token] = t
paddy@91 116 return nil
paddy@91 117 }
paddy@91 118
paddy@57 119 func (m *memstore) getTokensByProfileID(profileID uuid.ID, num, offset int) ([]Token, error) {
paddy@28 120 ids, err := m.lookupTokensByProfileID(profileID.String())
paddy@28 121 if err != nil {
paddy@28 122 return []Token{}, err
paddy@28 123 }
paddy@28 124 if len(ids) > num+offset {
paddy@28 125 ids = ids[offset : num+offset]
paddy@28 126 } else if len(ids) > offset {
paddy@28 127 ids = ids[offset:]
paddy@28 128 } else {
paddy@28 129 return []Token{}, nil
paddy@28 130 }
paddy@28 131 tokens := []Token{}
paddy@28 132 for _, id := range ids {
paddy@57 133 token, err := m.getToken(id, false)
paddy@28 134 if err != nil {
paddy@28 135 return []Token{}, err
paddy@28 136 }
paddy@28 137 tokens = append(tokens, token)
paddy@28 138 }
paddy@28 139 return tokens, nil
paddy@28 140 }
paddy@123 141
paddy@135 142 func refreshTokenValidate(w http.ResponseWriter, r *http.Request, context Context) (scopes []string, profileID uuid.ID, valid bool) {
paddy@123 143 enc := json.NewEncoder(w)
paddy@123 144 refresh := r.PostFormValue("refresh_token")
paddy@123 145 if refresh == "" {
paddy@123 146 w.WriteHeader(http.StatusBadRequest)
paddy@123 147 renderJSONError(enc, "invalid_request")
paddy@123 148 return
paddy@123 149 }
paddy@123 150 token, err := context.GetToken(refresh, true)
paddy@123 151 if err != nil {
paddy@123 152 if err == ErrTokenNotFound {
paddy@123 153 w.WriteHeader(http.StatusBadRequest)
paddy@123 154 renderJSONError(enc, "invalid_grant")
paddy@123 155 return
paddy@123 156 }
paddy@123 157 log.Println("Error exchanging refresh token:", err)
paddy@123 158 w.WriteHeader(http.StatusInternalServerError)
paddy@123 159 renderJSONError(enc, "server_error")
paddy@123 160 return
paddy@123 161 }
paddy@123 162 clientID, _, ok := getClientAuth(w, r, true)
paddy@123 163 if !ok {
paddy@123 164 return
paddy@123 165 }
paddy@123 166 if !token.ClientID.Equal(clientID) {
paddy@123 167 w.WriteHeader(http.StatusBadRequest)
paddy@123 168 renderJSONError(enc, "invalid_grant")
paddy@123 169 return
paddy@123 170 }
paddy@123 171 if token.RefreshRevoked {
paddy@123 172 w.WriteHeader(http.StatusBadRequest)
paddy@123 173 renderJSONError(enc, "invalid_grant")
paddy@123 174 return
paddy@123 175 }
paddy@135 176 return token.Scopes, token.ProfileID, true
paddy@123 177 }
paddy@123 178
paddy@123 179 func refreshTokenInvalidate(r *http.Request, context Context) error {
paddy@123 180 refresh := r.PostFormValue("refresh_token")
paddy@123 181 if refresh == "" {
paddy@123 182 return ErrTokenNotFound
paddy@123 183 }
paddy@123 184 return context.RevokeToken(refresh, true)
paddy@123 185 }
paddy@124 186
paddy@124 187 func refreshTokenAuditString(r *http.Request) string {
paddy@124 188 return "refresh_token:" + r.PostFormValue("refresh_token")
paddy@124 189 }
paddy@128 190
paddy@128 191 // BUG(paddy): We need to implement a handler for revoking a token.
paddy@128 192 // BUG(paddy): We need to implement a handler for listing active tokens.