11097: Merge branch 'master' into 11097-reuse-impure
[arvados.git] / sdk / go / auth / auth.go
1 package auth
2
3 import (
4         "encoding/base64"
5         "net/http"
6         "net/url"
7         "strings"
8 )
9
10 type Credentials struct {
11         Tokens []string
12 }
13
14 func NewCredentials() *Credentials {
15         return &Credentials{Tokens: []string{}}
16 }
17
18 func NewCredentialsFromHTTPRequest(r *http.Request) *Credentials {
19         c := NewCredentials()
20         c.LoadTokensFromHTTPRequest(r)
21         return c
22 }
23
24 // EncodeTokenCookie accepts a token and returns a byte slice suitable
25 // for use as a cookie value, such that it will be decoded correctly
26 // by LoadTokensFromHTTPRequest.
27 var EncodeTokenCookie func([]byte) string = base64.URLEncoding.EncodeToString
28
29 // DecodeTokenCookie accepts a cookie value and returns the encoded
30 // token.
31 var DecodeTokenCookie func(string) ([]byte, error) = base64.URLEncoding.DecodeString
32
33 // LoadTokensFromHttpRequest loads all tokens it can find in the
34 // headers and query string of an http query.
35 func (a *Credentials) LoadTokensFromHTTPRequest(r *http.Request) {
36         // Load plain token from "Authorization: OAuth2 ..." header
37         // (typically used by smart API clients)
38         if toks := strings.SplitN(r.Header.Get("Authorization"), " ", 2); len(toks) == 2 && toks[0] == "OAuth2" {
39                 a.Tokens = append(a.Tokens, toks[1])
40         }
41
42         // Load base64-encoded token from "Authorization: Basic ..."
43         // header (typically used by git via credential helper)
44         if _, password, ok := BasicAuth(r); ok {
45                 a.Tokens = append(a.Tokens, password)
46         }
47
48         // Load tokens from query string. It's generally not a good
49         // idea to pass tokens around this way, but passing a narrowly
50         // scoped token is a reasonable way to implement "secret link
51         // to an object" in a generic way.
52         //
53         // ParseQuery always returns a non-nil map which might have
54         // valid parameters, even when a decoding error causes it to
55         // return a non-nil err. We ignore err; hopefully the caller
56         // will also need to parse the query string for
57         // application-specific purposes and will therefore
58         // find/report decoding errors in a suitable way.
59         qvalues, _ := url.ParseQuery(r.URL.RawQuery)
60         if val, ok := qvalues["api_token"]; ok {
61                 a.Tokens = append(a.Tokens, val...)
62         }
63
64         a.loadTokenFromCookie(r)
65
66         // TODO: Load token from Rails session cookie (if Rails site
67         // secret is known)
68 }
69
70 func (a *Credentials) loadTokenFromCookie(r *http.Request) {
71         cookie, err := r.Cookie("arvados_api_token")
72         if err != nil || len(cookie.Value) == 0 {
73                 return
74         }
75         token, err := DecodeTokenCookie(cookie.Value)
76         if err != nil {
77                 return
78         }
79         a.Tokens = append(a.Tokens, string(token))
80 }
81
82 // TODO: LoadTokensFromHttpRequestBody(). We can't assume in
83 // LoadTokensFromHttpRequest() that [or how] we should read and parse
84 // the request body. This has to be requested explicitly by the
85 // application.