Merge branch '21898-info-button-behavior'
[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         "context"
9         "encoding/base64"
10         "net/http"
11         "net/url"
12         "strings"
13 )
14
15 type Credentials struct {
16         Tokens []string
17 }
18
19 func NewCredentials(tokens ...string) *Credentials {
20         return &Credentials{Tokens: tokens}
21 }
22
23 func NewContext(ctx context.Context, c *Credentials) context.Context {
24         return context.WithValue(ctx, contextKeyCredentials{}, c)
25 }
26
27 func FromContext(ctx context.Context) (*Credentials, bool) {
28         c, ok := ctx.Value(contextKeyCredentials{}).(*Credentials)
29         return c, ok
30 }
31
32 func CredentialsFromRequest(r *http.Request) *Credentials {
33         if c, ok := FromContext(r.Context()); ok {
34                 // preloaded by middleware
35                 return c
36         }
37         c := NewCredentials()
38         c.LoadTokensFromHTTPRequest(r)
39         return c
40 }
41
42 // EncodeTokenCookie accepts a token and returns a byte slice suitable
43 // for use as a cookie value, such that it will be decoded correctly
44 // by LoadTokensFromHTTPRequest.
45 var EncodeTokenCookie func([]byte) string = base64.URLEncoding.EncodeToString
46
47 // DecodeTokenCookie accepts a cookie value and returns the encoded
48 // token.
49 var DecodeTokenCookie func(string) ([]byte, error) = base64.URLEncoding.DecodeString
50
51 // LoadTokensFromHTTPRequest loads all tokens it can find in the
52 // headers and query string of an http query.
53 func (a *Credentials) LoadTokensFromHTTPRequest(r *http.Request) {
54         // Load plain token from "Authorization: Bearer ..." header
55         // (typically used by smart API clients).  Note many pre-3.0
56         // clients send "OAuth2 ..." instead of "Bearer ..." and that
57         // is still accepted.
58         if toks := strings.SplitN(r.Header.Get("Authorization"), " ", 2); len(toks) == 2 && (toks[0] == "OAuth2" || toks[0] == "Bearer") {
59                 a.Tokens = append(a.Tokens, strings.TrimSpace(toks[1]))
60         }
61
62         // Load base64-encoded token from "Authorization: Basic ..."
63         // header (typically used by git via credential helper)
64         if _, password, ok := r.BasicAuth(); ok {
65                 a.Tokens = append(a.Tokens, strings.TrimSpace(password))
66         }
67
68         // Load tokens from query string. It's generally not a good
69         // idea to pass tokens around this way, but passing a narrowly
70         // scoped token is a reasonable way to implement "secret link
71         // to an object" in a generic way.
72         //
73         // ParseQuery always returns a non-nil map which might have
74         // valid parameters, even when a decoding error causes it to
75         // return a non-nil err. We ignore err; hopefully the caller
76         // will also need to parse the query string for
77         // application-specific purposes and will therefore
78         // find/report decoding errors in a suitable way.
79         qvalues, _ := url.ParseQuery(r.URL.RawQuery)
80         if val, ok := qvalues["api_token"]; ok {
81                 for _, token := range val {
82                         a.Tokens = append(a.Tokens, strings.TrimSpace(token))
83                 }
84         }
85
86         a.loadTokenFromCookie(r)
87
88         // TODO: Load token from Rails session cookie (if Rails site
89         // secret is known)
90 }
91
92 func (a *Credentials) loadTokenFromCookie(r *http.Request) {
93         cookie, err := r.Cookie("arvados_api_token")
94         if err != nil || len(cookie.Value) == 0 {
95                 return
96         }
97         token, err := DecodeTokenCookie(cookie.Value)
98         if err != nil {
99                 return
100         }
101         a.Tokens = append(a.Tokens, strings.TrimSpace(string(token)))
102 }
103
104 // LoadTokensFromHTTPRequestBody loads credentials from the request
105 // body.
106 //
107 // This is separate from LoadTokensFromHTTPRequest() because it's not
108 // always desirable to read the request body. This has to be requested
109 // explicitly by the application.
110 func (a *Credentials) LoadTokensFromHTTPRequestBody(r *http.Request) error {
111         if r.Header.Get("Content-Type") != "application/x-www-form-urlencoded" {
112                 return nil
113         }
114         if err := r.ParseForm(); err != nil {
115                 return err
116         }
117         if t := r.PostFormValue("api_token"); t != "" {
118                 a.Tokens = append(a.Tokens, strings.TrimSpace(t))
119         }
120         return nil
121 }