auth
auth/context.go
Write Session tests. Add loginURI as a property to our Context, to keep track of where users should be redirected to log in. Implement the sessionStore in the memstore to let us test with Sessions. Catch when the HTTP Basic Auth header doesn't include two parts, rather than panicking. Return an ErrInvalidAuthFormat. Clean up the error handling for checkCookie to be cleaner. Log unexpected errors from request.Cookie. Stop checking for cookie expiration times--those aren't sent to the server, so we'll never get a valid session if we look for them. Add a helper to build a login redirect URI--a URI the user can be redirected to that has a URL-encoded URL to redirect the user back to after a successful login. Add a wrapper to wrap our Context into HTTP handlers. Create a RegisterOAuth2 helper that adds the OAuth2 endpoints to a Gorilla/mux router. Redirect users to the login page when they have no session set or an invalid session. Return a server error when we can't check our cookie for whatever reason. Log errors. Add sessions to our OAuth2 tests so the tests stop failing--the session check was interfering with them. Add a test for our getBasicAuth helper to ensure that we're parsing basic auth correctly. Add an ErrSessionAlreadyExists error to be returned when a session has an ID conflict. Test that our memstore implementation of the sessionStore works as intended..
| paddy@50 | 1 package auth |
| paddy@50 | 2 |
| paddy@50 | 3 import ( |
| paddy@50 | 4 "html/template" |
| paddy@50 | 5 "io" |
| paddy@55 | 6 "log" |
| paddy@77 | 7 "net/url" |
| paddy@50 | 8 "time" |
| paddy@50 | 9 |
| paddy@50 | 10 "code.secondbit.org/uuid" |
| paddy@50 | 11 ) |
| paddy@50 | 12 |
| paddy@57 | 13 // Context wraps the different storage interfaces and should |
| paddy@57 | 14 // be used as the main point of interaction for the data storage |
| paddy@57 | 15 // layer. |
| paddy@50 | 16 type Context struct { |
| paddy@50 | 17 template *template.Template |
| paddy@77 | 18 loginURI *url.URL |
| paddy@57 | 19 clients clientStore |
| paddy@57 | 20 grants grantStore |
| paddy@57 | 21 profiles profileStore |
| paddy@57 | 22 tokens tokenStore |
| paddy@69 | 23 sessions sessionStore |
| paddy@50 | 24 } |
| paddy@50 | 25 |
| paddy@57 | 26 // Render uses the HTML templates associated with the Context to render the |
| paddy@57 | 27 // template specified by name to out using data to fill any template variables. |
| paddy@55 | 28 func (c Context) Render(out io.Writer, name string, data interface{}) { |
| paddy@50 | 29 if c.template == nil { |
| paddy@57 | 30 log.Println("No template set on Context, can't render anything!") |
| paddy@57 | 31 return |
| paddy@50 | 32 } |
| paddy@55 | 33 err := c.template.ExecuteTemplate(out, name, data) |
| paddy@55 | 34 if err != nil { |
| paddy@55 | 35 log.Println("Error executing template", name, ":", err) |
| paddy@55 | 36 } |
| paddy@50 | 37 } |
| paddy@50 | 38 |
| paddy@57 | 39 // GetClient returns a single Client by its ID from the |
| paddy@57 | 40 // clientStore associated with the Context. |
| paddy@50 | 41 func (c Context) GetClient(id uuid.ID) (Client, error) { |
| paddy@50 | 42 if c.clients == nil { |
| paddy@50 | 43 return Client{}, ErrNoClientStore |
| paddy@50 | 44 } |
| paddy@57 | 45 return c.clients.getClient(id) |
| paddy@50 | 46 } |
| paddy@50 | 47 |
| paddy@57 | 48 // SaveClient stores the passed Client in the clientStore |
| paddy@57 | 49 // associated with the Context. |
| paddy@50 | 50 func (c Context) SaveClient(client Client) error { |
| paddy@50 | 51 if c.clients == nil { |
| paddy@50 | 52 return ErrNoClientStore |
| paddy@50 | 53 } |
| paddy@57 | 54 return c.clients.saveClient(client) |
| paddy@50 | 55 } |
| paddy@50 | 56 |
| paddy@57 | 57 // UpdateClient applies the specified ClientChange to the Client |
| paddy@57 | 58 // with the specified ID in the clientStore associated with the |
| paddy@57 | 59 // Context. |
| paddy@50 | 60 func (c Context) UpdateClient(id uuid.ID, change ClientChange) error { |
| paddy@50 | 61 if c.clients == nil { |
| paddy@50 | 62 return ErrNoClientStore |
| paddy@50 | 63 } |
| paddy@57 | 64 return c.clients.updateClient(id, change) |
| paddy@50 | 65 } |
| paddy@50 | 66 |
| paddy@57 | 67 // DeleteClient removes the client with the specified ID from the |
| paddy@57 | 68 // clientStore associated with the Context. |
| paddy@50 | 69 func (c Context) DeleteClient(id uuid.ID) error { |
| paddy@50 | 70 if c.clients == nil { |
| paddy@50 | 71 return ErrNoClientStore |
| paddy@50 | 72 } |
| paddy@57 | 73 return c.clients.deleteClient(id) |
| paddy@50 | 74 } |
| paddy@50 | 75 |
| paddy@57 | 76 // ListClientsByOwner returns a slice of up to num Clients, starting at offset (inclusive) |
| paddy@57 | 77 // that have the specified OwnerID in the clientStore associated with the Context. |
| paddy@50 | 78 func (c Context) ListClientsByOwner(ownerID uuid.ID, num, offset int) ([]Client, error) { |
| paddy@50 | 79 if c.clients == nil { |
| paddy@50 | 80 return []Client{}, ErrNoClientStore |
| paddy@50 | 81 } |
| paddy@57 | 82 return c.clients.listClientsByOwner(ownerID, num, offset) |
| paddy@50 | 83 } |
| paddy@50 | 84 |
| paddy@57 | 85 // AddEndpoint stores the specified Endpoint in the clientStore associated with the Context, |
| paddy@57 | 86 // and associates the newly-stored Endpoint with the Client specified by the passed ID. |
| paddy@50 | 87 func (c Context) AddEndpoint(client uuid.ID, endpoint Endpoint) error { |
| paddy@50 | 88 if c.clients == nil { |
| paddy@50 | 89 return ErrNoClientStore |
| paddy@50 | 90 } |
| paddy@57 | 91 return c.clients.addEndpoint(client, endpoint) |
| paddy@50 | 92 } |
| paddy@50 | 93 |
| paddy@57 | 94 // RemoveEndpoint deletes the Endpoint with the specified ID from the clientStore associated |
| paddy@57 | 95 // with the Context, and disassociates the Endpoint from the specified Client. |
| paddy@50 | 96 func (c Context) RemoveEndpoint(client, endpoint uuid.ID) error { |
| paddy@50 | 97 if c.clients == nil { |
| paddy@50 | 98 return ErrNoClientStore |
| paddy@50 | 99 } |
| paddy@57 | 100 return c.clients.removeEndpoint(client, endpoint) |
| paddy@50 | 101 } |
| paddy@50 | 102 |
| paddy@57 | 103 // CheckEndpoint finds Endpoints in the clientStore associated with the Context that belong |
| paddy@58 | 104 // to the Client specified by the passed ID and match the URI passed. URI matches must be |
| paddy@58 | 105 // performed according to RFC 3986 Section 6. |
| paddy@58 | 106 func (c Context) CheckEndpoint(client uuid.ID, URI string) (bool, error) { |
| paddy@50 | 107 if c.clients == nil { |
| paddy@50 | 108 return false, ErrNoClientStore |
| paddy@50 | 109 } |
| paddy@58 | 110 return c.clients.checkEndpoint(client, URI) |
| paddy@50 | 111 } |
| paddy@50 | 112 |
| paddy@57 | 113 // ListEndpoints finds Endpoints in the clientStore associated with the Context that belong |
| paddy@57 | 114 // to the Client specified by the passed ID. It returns up to num endpoints, starting at offset, |
| paddy@57 | 115 // exclusive. |
| paddy@50 | 116 func (c Context) ListEndpoints(client uuid.ID, num, offset int) ([]Endpoint, error) { |
| paddy@50 | 117 if c.clients == nil { |
| paddy@50 | 118 return []Endpoint{}, ErrNoClientStore |
| paddy@50 | 119 } |
| paddy@57 | 120 return c.clients.listEndpoints(client, num, offset) |
| paddy@50 | 121 } |
| paddy@50 | 122 |
| paddy@57 | 123 // CountEndpoints returns the number of Endpoints the are associated with the Client specified by the |
| paddy@57 | 124 // passed ID in the clientStore associated with the Context. |
| paddy@55 | 125 func (c Context) CountEndpoints(client uuid.ID) (int64, error) { |
| paddy@55 | 126 if c.clients == nil { |
| paddy@55 | 127 return 0, ErrNoClientStore |
| paddy@55 | 128 } |
| paddy@57 | 129 return c.clients.countEndpoints(client) |
| paddy@55 | 130 } |
| paddy@55 | 131 |
| paddy@57 | 132 // GetGrant returns the Grant specified by the provided code from the grantStore associated with the |
| paddy@57 | 133 // Context. |
| paddy@50 | 134 func (c Context) GetGrant(code string) (Grant, error) { |
| paddy@50 | 135 if c.grants == nil { |
| paddy@50 | 136 return Grant{}, ErrNoGrantStore |
| paddy@50 | 137 } |
| paddy@57 | 138 return c.grants.getGrant(code) |
| paddy@50 | 139 } |
| paddy@50 | 140 |
| paddy@57 | 141 // SaveGrant stores the passed Grant in the grantStore associated with the Context. |
| paddy@50 | 142 func (c Context) SaveGrant(grant Grant) error { |
| paddy@50 | 143 if c.grants == nil { |
| paddy@50 | 144 return ErrNoGrantStore |
| paddy@50 | 145 } |
| paddy@57 | 146 return c.grants.saveGrant(grant) |
| paddy@50 | 147 } |
| paddy@50 | 148 |
| paddy@57 | 149 // DeleteGrant removes the Grant specified by the provided code from the grantStore associated with |
| paddy@57 | 150 // the Context. |
| paddy@50 | 151 func (c Context) DeleteGrant(code string) error { |
| paddy@50 | 152 if c.grants == nil { |
| paddy@50 | 153 return ErrNoGrantStore |
| paddy@50 | 154 } |
| paddy@57 | 155 return c.grants.deleteGrant(code) |
| paddy@50 | 156 } |
| paddy@50 | 157 |
| paddy@57 | 158 // GetProfileByID returns the Profile specified by the provided ID from the profileStore associated with |
| paddy@57 | 159 // the Context. |
| paddy@50 | 160 func (c Context) GetProfileByID(id uuid.ID) (Profile, error) { |
| paddy@50 | 161 if c.profiles == nil { |
| paddy@50 | 162 return Profile{}, ErrNoProfileStore |
| paddy@50 | 163 } |
| paddy@57 | 164 return c.profiles.getProfileByID(id) |
| paddy@50 | 165 } |
| paddy@50 | 166 |
| paddy@57 | 167 // GetProfileByLogin returns the Profile associated with the specified Login from the profileStore associated |
| paddy@57 | 168 // with the Context. |
| paddy@69 | 169 func (c Context) GetProfileByLogin(value string) (Profile, error) { |
| paddy@50 | 170 if c.profiles == nil { |
| paddy@50 | 171 return Profile{}, ErrNoProfileStore |
| paddy@50 | 172 } |
| paddy@69 | 173 return c.profiles.getProfileByLogin(value) |
| paddy@50 | 174 } |
| paddy@50 | 175 |
| paddy@57 | 176 // SaveProfile inserts the passed Profile into the profileStore associated with the Context. |
| paddy@50 | 177 func (c Context) SaveProfile(profile Profile) error { |
| paddy@50 | 178 if c.profiles == nil { |
| paddy@50 | 179 return ErrNoProfileStore |
| paddy@50 | 180 } |
| paddy@57 | 181 return c.profiles.saveProfile(profile) |
| paddy@50 | 182 } |
| paddy@50 | 183 |
| paddy@57 | 184 // UpdateProfile applies the supplied ProfileChange to the Profile that matches the specified ID |
| paddy@57 | 185 // in the profileStore associated with the Context. |
| paddy@50 | 186 func (c Context) UpdateProfile(id uuid.ID, change ProfileChange) error { |
| paddy@50 | 187 if c.profiles == nil { |
| paddy@50 | 188 return ErrNoProfileStore |
| paddy@50 | 189 } |
| paddy@57 | 190 return c.profiles.updateProfile(id, change) |
| paddy@50 | 191 } |
| paddy@50 | 192 |
| paddy@57 | 193 // UpdateProfiles applies the supplied BulkProfileChange to every Profile that matches one of the |
| paddy@57 | 194 // specified IDs in the profileStore associated with the Context. |
| paddy@50 | 195 func (c Context) UpdateProfiles(ids []uuid.ID, change BulkProfileChange) error { |
| paddy@50 | 196 if c.profiles == nil { |
| paddy@50 | 197 return ErrNoProfileStore |
| paddy@50 | 198 } |
| paddy@57 | 199 return c.profiles.updateProfiles(ids, change) |
| paddy@50 | 200 } |
| paddy@50 | 201 |
| paddy@57 | 202 // DeleteProfile removes the Profile specified by the passed ID from the profileStore associated |
| paddy@57 | 203 // with the Context. |
| paddy@50 | 204 func (c Context) DeleteProfile(id uuid.ID) error { |
| paddy@50 | 205 if c.profiles == nil { |
| paddy@50 | 206 return ErrNoProfileStore |
| paddy@50 | 207 } |
| paddy@57 | 208 return c.profiles.deleteProfile(id) |
| paddy@50 | 209 } |
| paddy@50 | 210 |
| paddy@57 | 211 // AddLogin stores the passed Login in the profileStore associated with the Context. It also associates |
| paddy@57 | 212 // the newly-created Login with the Orofile in login.ProfileID. |
| paddy@50 | 213 func (c Context) AddLogin(login Login) error { |
| paddy@50 | 214 if c.profiles == nil { |
| paddy@50 | 215 return ErrNoProfileStore |
| paddy@50 | 216 } |
| paddy@57 | 217 return c.profiles.addLogin(login) |
| paddy@50 | 218 } |
| paddy@50 | 219 |
| paddy@57 | 220 // RemoveLogin removes the specified Login from the profileStore associated with the Context, provided |
| paddy@57 | 221 // the Login has a ProfileID property that matches the profile ID passed in. It also disassociates the |
| paddy@57 | 222 // deleted Login from the Profile in login.ProfileID. |
| paddy@69 | 223 func (c Context) RemoveLogin(value string, profile uuid.ID) error { |
| paddy@50 | 224 if c.profiles == nil { |
| paddy@50 | 225 return ErrNoProfileStore |
| paddy@50 | 226 } |
| paddy@69 | 227 return c.profiles.removeLogin(value, profile) |
| paddy@50 | 228 } |
| paddy@50 | 229 |
| paddy@57 | 230 // RecordLoginUse sets the LastUsed property of the Login specified in the profileStore associated with |
| paddy@57 | 231 // the Context to the value passed in as when. |
| paddy@69 | 232 func (c Context) RecordLoginUse(value string, when time.Time) error { |
| paddy@50 | 233 if c.profiles == nil { |
| paddy@50 | 234 return ErrNoProfileStore |
| paddy@50 | 235 } |
| paddy@69 | 236 return c.profiles.recordLoginUse(value, when) |
| paddy@50 | 237 } |
| paddy@50 | 238 |
| paddy@57 | 239 // ListLogins returns a slice of up to num Logins associated with the specified Profile from the profileStore |
| paddy@57 | 240 // associated with the Context, skipping offset Profiles. |
| paddy@50 | 241 func (c Context) ListLogins(profile uuid.ID, num, offset int) ([]Login, error) { |
| paddy@50 | 242 if c.profiles == nil { |
| paddy@50 | 243 return []Login{}, ErrNoProfileStore |
| paddy@50 | 244 } |
| paddy@57 | 245 return c.profiles.listLogins(profile, num, offset) |
| paddy@50 | 246 } |
| paddy@50 | 247 |
| paddy@57 | 248 // GetToken returns the Token specified from the tokenStore associated with the Context. |
| paddy@57 | 249 // If refresh is true, the token input should be compared against the refresh tokens, not the |
| paddy@57 | 250 // access tokens. |
| paddy@50 | 251 func (c Context) GetToken(token string, refresh bool) (Token, error) { |
| paddy@50 | 252 if c.tokens == nil { |
| paddy@50 | 253 return Token{}, ErrNoTokenStore |
| paddy@50 | 254 } |
| paddy@57 | 255 return c.tokens.getToken(token, refresh) |
| paddy@50 | 256 } |
| paddy@50 | 257 |
| paddy@57 | 258 // SaveToken stores the passed Token in the tokenStore associated with the Context. |
| paddy@50 | 259 func (c Context) SaveToken(token Token) error { |
| paddy@50 | 260 if c.tokens == nil { |
| paddy@50 | 261 return ErrNoTokenStore |
| paddy@50 | 262 } |
| paddy@57 | 263 return c.tokens.saveToken(token) |
| paddy@50 | 264 } |
| paddy@50 | 265 |
| paddy@57 | 266 // RemoveToken removes the Token identified by the passed token string from the tokenStore associated |
| paddy@57 | 267 // with the Context. |
| paddy@50 | 268 func (c Context) RemoveToken(token string) error { |
| paddy@50 | 269 if c.tokens == nil { |
| paddy@50 | 270 return ErrNoTokenStore |
| paddy@50 | 271 } |
| paddy@57 | 272 return c.tokens.removeToken(token) |
| paddy@50 | 273 } |
| paddy@50 | 274 |
| paddy@57 | 275 // GetTokensByProfileID returns a slice of up to num Tokens with a ProfileID that matches the specified |
| paddy@57 | 276 // profileID from the tokenStore associated with the Context, skipping offset Tokens. |
| paddy@50 | 277 func (c Context) GetTokensByProfileID(profileID uuid.ID, num, offset int) ([]Token, error) { |
| paddy@50 | 278 if c.tokens == nil { |
| paddy@50 | 279 return []Token{}, ErrNoTokenStore |
| paddy@50 | 280 } |
| paddy@57 | 281 return c.tokens.getTokensByProfileID(profileID, num, offset) |
| paddy@50 | 282 } |
| paddy@69 | 283 |
| paddy@69 | 284 // CreateSession stores the passed Session in the sessionStore associated with the Context. |
| paddy@69 | 285 func (c Context) CreateSession(session Session) error { |
| paddy@69 | 286 if c.sessions == nil { |
| paddy@69 | 287 return ErrNoSessionStore |
| paddy@69 | 288 } |
| paddy@69 | 289 return c.sessions.createSession(session) |
| paddy@69 | 290 } |
| paddy@69 | 291 |
| paddy@69 | 292 // GetSession returns the Session specified from the sessionStore associated with the Context. |
| paddy@69 | 293 func (c Context) GetSession(id string) (Session, error) { |
| paddy@69 | 294 if c.sessions == nil { |
| paddy@69 | 295 return Session{}, ErrNoSessionStore |
| paddy@69 | 296 } |
| paddy@69 | 297 return c.sessions.getSession(id) |
| paddy@69 | 298 } |
| paddy@69 | 299 |
| paddy@69 | 300 // RemoveSession removes the Session identified by the passed ID from the sessionStore associated with |
| paddy@69 | 301 // the Context. |
| paddy@69 | 302 func (c Context) RemoveSession(id string) error { |
| paddy@69 | 303 if c.sessions == nil { |
| paddy@69 | 304 return ErrNoSessionStore |
| paddy@69 | 305 } |
| paddy@69 | 306 return c.sessions.removeSession(id) |
| paddy@69 | 307 } |
| paddy@69 | 308 |
| paddy@69 | 309 // ListSessions returns a slice of up to num Sessions from the sessionStore associated with the Context, |
| paddy@69 | 310 // ordered by the date they were created, descending. If before.IsZero() returns false, only Sessions |
| paddy@69 | 311 // that were created before that time will be returned. If profile is not nil, only Sessions belonging to |
| paddy@69 | 312 // that Profile will be returned. |
| paddy@69 | 313 func (c Context) ListSessions(profile uuid.ID, before time.Time, num int64) ([]Session, error) { |
| paddy@69 | 314 if c.sessions != nil { |
| paddy@69 | 315 return []Session{}, ErrNoSessionStore |
| paddy@69 | 316 } |
| paddy@69 | 317 return c.sessions.listSessions(profile, before, num) |
| paddy@69 | 318 } |