auth
auth/context.go
Remove refresh token expiration, update implicit token. Refresh tokens no longer expire, because they're supposed to be long-lived, and we have no way to communicate to the user exactly how long-lived they are. Instead, they are invalidated after a single use, which should prevent too much abuse. It gives them an effective lifespan of "default token expiration, or until used", which I think is Good Enough. Also updated our implicit token to set the CreatedFrom to "implicit" and the ClientID to the client ID, which is important, I guess. It's really annoying that we have that logic in two different places.
| 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@107 | 10 "code.secondbit.org/uuid.hg" |
| 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@87 | 17 template *template.Template |
| paddy@87 | 18 loginURI *url.URL |
| paddy@87 | 19 clients clientStore |
| paddy@87 | 20 authCodes authorizationCodeStore |
| paddy@87 | 21 profiles profileStore |
| paddy@87 | 22 tokens tokenStore |
| paddy@87 | 23 sessions sessionStore |
| paddy@96 | 24 config Config |
| paddy@96 | 25 } |
| paddy@96 | 26 |
| paddy@96 | 27 // NewContext takes a Config instance and uses it to bootstrap a Context |
| paddy@96 | 28 // using the information provided in the Config variable. |
| paddy@96 | 29 func NewContext(config Config) (Context, error) { |
| paddy@102 | 30 if config.iterations == 0 { |
| paddy@102 | 31 return Context{}, ErrConfigNotInitialized |
| paddy@102 | 32 } |
| paddy@96 | 33 context := Context{ |
| paddy@96 | 34 clients: config.ClientStore, |
| paddy@96 | 35 authCodes: config.AuthCodeStore, |
| paddy@96 | 36 profiles: config.ProfileStore, |
| paddy@96 | 37 tokens: config.TokenStore, |
| paddy@96 | 38 sessions: config.SessionStore, |
| paddy@96 | 39 template: config.Template, |
| paddy@96 | 40 config: config, |
| paddy@96 | 41 } |
| paddy@96 | 42 var err error |
| paddy@96 | 43 context.loginURI, err = url.Parse(config.LoginURI) |
| paddy@96 | 44 if err != nil { |
| paddy@96 | 45 log.Println(err) |
| paddy@96 | 46 return Context{}, ErrInvalidLoginURI |
| paddy@96 | 47 } |
| paddy@96 | 48 return context, nil |
| paddy@50 | 49 } |
| paddy@50 | 50 |
| paddy@57 | 51 // Render uses the HTML templates associated with the Context to render the |
| paddy@57 | 52 // template specified by name to out using data to fill any template variables. |
| paddy@55 | 53 func (c Context) Render(out io.Writer, name string, data interface{}) { |
| paddy@50 | 54 if c.template == nil { |
| paddy@57 | 55 log.Println("No template set on Context, can't render anything!") |
| paddy@57 | 56 return |
| paddy@50 | 57 } |
| paddy@55 | 58 err := c.template.ExecuteTemplate(out, name, data) |
| paddy@55 | 59 if err != nil { |
| paddy@55 | 60 log.Println("Error executing template", name, ":", err) |
| paddy@55 | 61 } |
| paddy@50 | 62 } |
| paddy@50 | 63 |
| paddy@57 | 64 // GetClient returns a single Client by its ID from the |
| paddy@57 | 65 // clientStore associated with the Context. |
| paddy@50 | 66 func (c Context) GetClient(id uuid.ID) (Client, error) { |
| paddy@50 | 67 if c.clients == nil { |
| paddy@50 | 68 return Client{}, ErrNoClientStore |
| paddy@50 | 69 } |
| paddy@57 | 70 return c.clients.getClient(id) |
| paddy@50 | 71 } |
| paddy@50 | 72 |
| paddy@57 | 73 // SaveClient stores the passed Client in the clientStore |
| paddy@57 | 74 // associated with the Context. |
| paddy@50 | 75 func (c Context) SaveClient(client Client) error { |
| paddy@50 | 76 if c.clients == nil { |
| paddy@50 | 77 return ErrNoClientStore |
| paddy@50 | 78 } |
| paddy@57 | 79 return c.clients.saveClient(client) |
| paddy@50 | 80 } |
| paddy@50 | 81 |
| paddy@57 | 82 // UpdateClient applies the specified ClientChange to the Client |
| paddy@57 | 83 // with the specified ID in the clientStore associated with the |
| paddy@57 | 84 // Context. |
| paddy@50 | 85 func (c Context) UpdateClient(id uuid.ID, change ClientChange) error { |
| paddy@50 | 86 if c.clients == nil { |
| paddy@50 | 87 return ErrNoClientStore |
| paddy@50 | 88 } |
| paddy@57 | 89 return c.clients.updateClient(id, change) |
| paddy@50 | 90 } |
| paddy@50 | 91 |
| paddy@57 | 92 // DeleteClient removes the client with the specified ID from the |
| paddy@57 | 93 // clientStore associated with the Context. |
| paddy@50 | 94 func (c Context) DeleteClient(id uuid.ID) error { |
| paddy@50 | 95 if c.clients == nil { |
| paddy@50 | 96 return ErrNoClientStore |
| paddy@50 | 97 } |
| paddy@57 | 98 return c.clients.deleteClient(id) |
| paddy@50 | 99 } |
| paddy@50 | 100 |
| paddy@57 | 101 // ListClientsByOwner returns a slice of up to num Clients, starting at offset (inclusive) |
| paddy@57 | 102 // that have the specified OwnerID in the clientStore associated with the Context. |
| paddy@50 | 103 func (c Context) ListClientsByOwner(ownerID uuid.ID, num, offset int) ([]Client, error) { |
| paddy@50 | 104 if c.clients == nil { |
| paddy@50 | 105 return []Client{}, ErrNoClientStore |
| paddy@50 | 106 } |
| paddy@57 | 107 return c.clients.listClientsByOwner(ownerID, num, offset) |
| paddy@50 | 108 } |
| paddy@50 | 109 |
| paddy@115 | 110 // AddEndpoints stores the specified Endpoints in the clientStore associated with the Context, |
| paddy@115 | 111 // and associates the newly-stored Endpoints with the Client specified by the passed ID. |
| paddy@115 | 112 func (c Context) AddEndpoints(client uuid.ID, endpoints []Endpoint) error { |
| paddy@50 | 113 if c.clients == nil { |
| paddy@50 | 114 return ErrNoClientStore |
| paddy@50 | 115 } |
| paddy@116 | 116 for pos, endpoint := range endpoints { |
| paddy@116 | 117 u, err := normalizeURIString(endpoint.URI) |
| paddy@116 | 118 if err != nil { |
| paddy@116 | 119 return err |
| paddy@116 | 120 } |
| paddy@116 | 121 endpoint.NormalizedURI = u |
| paddy@116 | 122 endpoints[pos] = endpoint |
| paddy@116 | 123 } |
| paddy@115 | 124 return c.clients.addEndpoints(client, endpoints) |
| paddy@50 | 125 } |
| paddy@50 | 126 |
| paddy@57 | 127 // RemoveEndpoint deletes the Endpoint with the specified ID from the clientStore associated |
| paddy@57 | 128 // with the Context, and disassociates the Endpoint from the specified Client. |
| paddy@50 | 129 func (c Context) RemoveEndpoint(client, endpoint uuid.ID) error { |
| paddy@50 | 130 if c.clients == nil { |
| paddy@50 | 131 return ErrNoClientStore |
| paddy@50 | 132 } |
| paddy@57 | 133 return c.clients.removeEndpoint(client, endpoint) |
| paddy@50 | 134 } |
| paddy@50 | 135 |
| paddy@57 | 136 // CheckEndpoint finds Endpoints in the clientStore associated with the Context that belong |
| paddy@58 | 137 // to the Client specified by the passed ID and match the URI passed. URI matches must be |
| paddy@58 | 138 // performed according to RFC 3986 Section 6. |
| paddy@58 | 139 func (c Context) CheckEndpoint(client uuid.ID, URI string) (bool, error) { |
| paddy@50 | 140 if c.clients == nil { |
| paddy@50 | 141 return false, ErrNoClientStore |
| paddy@50 | 142 } |
| paddy@116 | 143 u, err := normalizeURIString(URI) |
| paddy@116 | 144 if err != nil { |
| paddy@116 | 145 return false, err |
| paddy@116 | 146 } |
| paddy@116 | 147 return c.clients.checkEndpoint(client, u) |
| paddy@50 | 148 } |
| paddy@50 | 149 |
| paddy@57 | 150 // ListEndpoints finds Endpoints in the clientStore associated with the Context that belong |
| paddy@57 | 151 // to the Client specified by the passed ID. It returns up to num endpoints, starting at offset, |
| paddy@57 | 152 // exclusive. |
| paddy@50 | 153 func (c Context) ListEndpoints(client uuid.ID, num, offset int) ([]Endpoint, error) { |
| paddy@50 | 154 if c.clients == nil { |
| paddy@50 | 155 return []Endpoint{}, ErrNoClientStore |
| paddy@50 | 156 } |
| paddy@57 | 157 return c.clients.listEndpoints(client, num, offset) |
| paddy@50 | 158 } |
| paddy@50 | 159 |
| paddy@57 | 160 // CountEndpoints returns the number of Endpoints the are associated with the Client specified by the |
| paddy@57 | 161 // passed ID in the clientStore associated with the Context. |
| paddy@55 | 162 func (c Context) CountEndpoints(client uuid.ID) (int64, error) { |
| paddy@55 | 163 if c.clients == nil { |
| paddy@55 | 164 return 0, ErrNoClientStore |
| paddy@55 | 165 } |
| paddy@57 | 166 return c.clients.countEndpoints(client) |
| paddy@55 | 167 } |
| paddy@55 | 168 |
| paddy@87 | 169 // GetAuthorizationCode returns the AuthorizationCode specified by the provided code from the authorizationCodeStore associated with the |
| paddy@57 | 170 // Context. |
| paddy@87 | 171 func (c Context) GetAuthorizationCode(code string) (AuthorizationCode, error) { |
| paddy@87 | 172 if c.authCodes == nil { |
| paddy@87 | 173 return AuthorizationCode{}, ErrNoAuthorizationCodeStore |
| paddy@50 | 174 } |
| paddy@87 | 175 return c.authCodes.getAuthorizationCode(code) |
| paddy@50 | 176 } |
| paddy@50 | 177 |
| paddy@87 | 178 // SaveAuthorizationCode stores the passed AuthorizationCode in the authorizationCodeStore associated with the Context. |
| paddy@87 | 179 func (c Context) SaveAuthorizationCode(authCode AuthorizationCode) error { |
| paddy@87 | 180 if c.authCodes == nil { |
| paddy@87 | 181 return ErrNoAuthorizationCodeStore |
| paddy@50 | 182 } |
| paddy@87 | 183 return c.authCodes.saveAuthorizationCode(authCode) |
| paddy@50 | 184 } |
| paddy@50 | 185 |
| paddy@87 | 186 // DeleteAuthorizationCode removes the AuthorizationCode specified by the provided code from the authorizationCodeStore associated with |
| paddy@57 | 187 // the Context. |
| paddy@87 | 188 func (c Context) DeleteAuthorizationCode(code string) error { |
| paddy@87 | 189 if c.authCodes == nil { |
| paddy@87 | 190 return ErrNoAuthorizationCodeStore |
| paddy@50 | 191 } |
| paddy@87 | 192 return c.authCodes.deleteAuthorizationCode(code) |
| paddy@50 | 193 } |
| paddy@50 | 194 |
| paddy@94 | 195 // UseAuthorizationCode marks the AuthorizationCode specified by the provided code as used in the authorizationCodeStore associated with |
| paddy@94 | 196 // the Context. Once an AuthorizationCode is marked as used, its Used property will be set to true when retrieved from the authorizationCodeStore. |
| paddy@94 | 197 func (c Context) UseAuthorizationCode(code string) error { |
| paddy@94 | 198 if c.authCodes == nil { |
| paddy@94 | 199 return ErrNoAuthorizationCodeStore |
| paddy@94 | 200 } |
| paddy@94 | 201 return c.authCodes.useAuthorizationCode(code) |
| paddy@94 | 202 } |
| paddy@94 | 203 |
| paddy@57 | 204 // GetProfileByID returns the Profile specified by the provided ID from the profileStore associated with |
| paddy@57 | 205 // the Context. |
| paddy@50 | 206 func (c Context) GetProfileByID(id uuid.ID) (Profile, error) { |
| paddy@50 | 207 if c.profiles == nil { |
| paddy@50 | 208 return Profile{}, ErrNoProfileStore |
| paddy@50 | 209 } |
| paddy@57 | 210 return c.profiles.getProfileByID(id) |
| paddy@50 | 211 } |
| paddy@50 | 212 |
| paddy@57 | 213 // GetProfileByLogin returns the Profile associated with the specified Login from the profileStore associated |
| paddy@57 | 214 // with the Context. |
| paddy@69 | 215 func (c Context) GetProfileByLogin(value string) (Profile, error) { |
| paddy@50 | 216 if c.profiles == nil { |
| paddy@50 | 217 return Profile{}, ErrNoProfileStore |
| paddy@50 | 218 } |
| paddy@69 | 219 return c.profiles.getProfileByLogin(value) |
| paddy@50 | 220 } |
| paddy@50 | 221 |
| paddy@57 | 222 // SaveProfile inserts the passed Profile into the profileStore associated with the Context. |
| paddy@50 | 223 func (c Context) SaveProfile(profile Profile) error { |
| paddy@50 | 224 if c.profiles == nil { |
| paddy@50 | 225 return ErrNoProfileStore |
| paddy@50 | 226 } |
| paddy@57 | 227 return c.profiles.saveProfile(profile) |
| paddy@50 | 228 } |
| paddy@50 | 229 |
| paddy@57 | 230 // UpdateProfile applies the supplied ProfileChange to the Profile that matches the specified ID |
| paddy@57 | 231 // in the profileStore associated with the Context. |
| paddy@50 | 232 func (c Context) UpdateProfile(id uuid.ID, change ProfileChange) error { |
| paddy@50 | 233 if c.profiles == nil { |
| paddy@50 | 234 return ErrNoProfileStore |
| paddy@50 | 235 } |
| paddy@57 | 236 return c.profiles.updateProfile(id, change) |
| paddy@50 | 237 } |
| paddy@50 | 238 |
| paddy@57 | 239 // UpdateProfiles applies the supplied BulkProfileChange to every Profile that matches one of the |
| paddy@57 | 240 // specified IDs in the profileStore associated with the Context. |
| paddy@50 | 241 func (c Context) UpdateProfiles(ids []uuid.ID, change BulkProfileChange) error { |
| paddy@50 | 242 if c.profiles == nil { |
| paddy@50 | 243 return ErrNoProfileStore |
| paddy@50 | 244 } |
| paddy@57 | 245 return c.profiles.updateProfiles(ids, change) |
| paddy@50 | 246 } |
| paddy@50 | 247 |
| paddy@57 | 248 // DeleteProfile removes the Profile specified by the passed ID from the profileStore associated |
| paddy@57 | 249 // with the Context. |
| paddy@50 | 250 func (c Context) DeleteProfile(id uuid.ID) error { |
| paddy@50 | 251 if c.profiles == nil { |
| paddy@50 | 252 return ErrNoProfileStore |
| paddy@50 | 253 } |
| paddy@57 | 254 return c.profiles.deleteProfile(id) |
| paddy@50 | 255 } |
| paddy@50 | 256 |
| paddy@57 | 257 // AddLogin stores the passed Login in the profileStore associated with the Context. It also associates |
| paddy@57 | 258 // the newly-created Login with the Orofile in login.ProfileID. |
| paddy@50 | 259 func (c Context) AddLogin(login Login) error { |
| paddy@50 | 260 if c.profiles == nil { |
| paddy@50 | 261 return ErrNoProfileStore |
| paddy@50 | 262 } |
| paddy@57 | 263 return c.profiles.addLogin(login) |
| paddy@50 | 264 } |
| paddy@50 | 265 |
| paddy@57 | 266 // RemoveLogin removes the specified Login from the profileStore associated with the Context, provided |
| paddy@57 | 267 // the Login has a ProfileID property that matches the profile ID passed in. It also disassociates the |
| paddy@57 | 268 // deleted Login from the Profile in login.ProfileID. |
| paddy@69 | 269 func (c Context) RemoveLogin(value string, profile uuid.ID) error { |
| paddy@50 | 270 if c.profiles == nil { |
| paddy@50 | 271 return ErrNoProfileStore |
| paddy@50 | 272 } |
| paddy@69 | 273 return c.profiles.removeLogin(value, profile) |
| paddy@50 | 274 } |
| paddy@50 | 275 |
| paddy@57 | 276 // RecordLoginUse sets the LastUsed property of the Login specified in the profileStore associated with |
| paddy@57 | 277 // the Context to the value passed in as when. |
| paddy@69 | 278 func (c Context) RecordLoginUse(value string, when time.Time) error { |
| paddy@50 | 279 if c.profiles == nil { |
| paddy@50 | 280 return ErrNoProfileStore |
| paddy@50 | 281 } |
| paddy@69 | 282 return c.profiles.recordLoginUse(value, when) |
| paddy@50 | 283 } |
| paddy@50 | 284 |
| paddy@57 | 285 // ListLogins returns a slice of up to num Logins associated with the specified Profile from the profileStore |
| paddy@57 | 286 // associated with the Context, skipping offset Profiles. |
| paddy@50 | 287 func (c Context) ListLogins(profile uuid.ID, num, offset int) ([]Login, error) { |
| paddy@50 | 288 if c.profiles == nil { |
| paddy@50 | 289 return []Login{}, ErrNoProfileStore |
| paddy@50 | 290 } |
| paddy@57 | 291 return c.profiles.listLogins(profile, num, offset) |
| paddy@50 | 292 } |
| paddy@50 | 293 |
| paddy@57 | 294 // GetToken returns the Token specified from the tokenStore associated with the Context. |
| paddy@57 | 295 // If refresh is true, the token input should be compared against the refresh tokens, not the |
| paddy@57 | 296 // access tokens. |
| paddy@50 | 297 func (c Context) GetToken(token string, refresh bool) (Token, error) { |
| paddy@50 | 298 if c.tokens == nil { |
| paddy@50 | 299 return Token{}, ErrNoTokenStore |
| paddy@50 | 300 } |
| paddy@57 | 301 return c.tokens.getToken(token, refresh) |
| paddy@50 | 302 } |
| paddy@50 | 303 |
| paddy@57 | 304 // SaveToken stores the passed Token in the tokenStore associated with the Context. |
| paddy@50 | 305 func (c Context) SaveToken(token Token) error { |
| paddy@50 | 306 if c.tokens == nil { |
| paddy@50 | 307 return ErrNoTokenStore |
| paddy@50 | 308 } |
| paddy@57 | 309 return c.tokens.saveToken(token) |
| paddy@50 | 310 } |
| paddy@50 | 311 |
| paddy@57 | 312 // RemoveToken removes the Token identified by the passed token string from the tokenStore associated |
| paddy@57 | 313 // with the Context. |
| paddy@50 | 314 func (c Context) RemoveToken(token string) error { |
| paddy@50 | 315 if c.tokens == nil { |
| paddy@50 | 316 return ErrNoTokenStore |
| paddy@50 | 317 } |
| paddy@57 | 318 return c.tokens.removeToken(token) |
| paddy@50 | 319 } |
| paddy@50 | 320 |
| paddy@93 | 321 // RevokeToken revokes the Token identfied by the passed token string from the tokenStore associated |
| paddy@93 | 322 // with the context. If refresh is true, the token input should be compared against the refresh tokens, |
| paddy@93 | 323 // not the access tokens. |
| paddy@93 | 324 func (c Context) RevokeToken(token string, refresh bool) error { |
| paddy@93 | 325 if c.tokens == nil { |
| paddy@93 | 326 return ErrNoTokenStore |
| paddy@93 | 327 } |
| paddy@93 | 328 return c.tokens.revokeToken(token, refresh) |
| paddy@93 | 329 } |
| paddy@93 | 330 |
| paddy@57 | 331 // GetTokensByProfileID returns a slice of up to num Tokens with a ProfileID that matches the specified |
| paddy@57 | 332 // profileID from the tokenStore associated with the Context, skipping offset Tokens. |
| paddy@50 | 333 func (c Context) GetTokensByProfileID(profileID uuid.ID, num, offset int) ([]Token, error) { |
| paddy@50 | 334 if c.tokens == nil { |
| paddy@50 | 335 return []Token{}, ErrNoTokenStore |
| paddy@50 | 336 } |
| paddy@57 | 337 return c.tokens.getTokensByProfileID(profileID, num, offset) |
| paddy@50 | 338 } |
| paddy@69 | 339 |
| paddy@69 | 340 // CreateSession stores the passed Session in the sessionStore associated with the Context. |
| paddy@69 | 341 func (c Context) CreateSession(session Session) error { |
| paddy@69 | 342 if c.sessions == nil { |
| paddy@69 | 343 return ErrNoSessionStore |
| paddy@69 | 344 } |
| paddy@69 | 345 return c.sessions.createSession(session) |
| paddy@69 | 346 } |
| paddy@69 | 347 |
| paddy@69 | 348 // GetSession returns the Session specified from the sessionStore associated with the Context. |
| paddy@69 | 349 func (c Context) GetSession(id string) (Session, error) { |
| paddy@69 | 350 if c.sessions == nil { |
| paddy@69 | 351 return Session{}, ErrNoSessionStore |
| paddy@69 | 352 } |
| paddy@69 | 353 return c.sessions.getSession(id) |
| paddy@69 | 354 } |
| paddy@69 | 355 |
| paddy@69 | 356 // RemoveSession removes the Session identified by the passed ID from the sessionStore associated with |
| paddy@69 | 357 // the Context. |
| paddy@69 | 358 func (c Context) RemoveSession(id string) error { |
| paddy@69 | 359 if c.sessions == nil { |
| paddy@69 | 360 return ErrNoSessionStore |
| paddy@69 | 361 } |
| paddy@69 | 362 return c.sessions.removeSession(id) |
| paddy@69 | 363 } |
| paddy@69 | 364 |
| paddy@69 | 365 // ListSessions returns a slice of up to num Sessions from the sessionStore associated with the Context, |
| paddy@69 | 366 // ordered by the date they were created, descending. If before.IsZero() returns false, only Sessions |
| paddy@69 | 367 // that were created before that time will be returned. If profile is not nil, only Sessions belonging to |
| paddy@69 | 368 // that Profile will be returned. |
| paddy@69 | 369 func (c Context) ListSessions(profile uuid.ID, before time.Time, num int64) ([]Session, error) { |
| paddy@116 | 370 if c.sessions == nil { |
| paddy@69 | 371 return []Session{}, ErrNoSessionStore |
| paddy@69 | 372 } |
| paddy@69 | 373 return c.sessions.listSessions(profile, before, num) |
| paddy@69 | 374 } |