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