1 /* 2 Copyright The ORAS Authors. 3 Licensed under the Apache License, Version 2.0 (the "License"); 4 you may not use this file except in compliance with the License. 5 You may obtain a copy of the License at 6 7 http://www.apache.org/licenses/LICENSE-2.0 8 9 Unless required by applicable law or agreed to in writing, software 10 distributed under the License is distributed on an "AS IS" BASIS, 11 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 See the License for the specific language governing permissions and 13 limitations under the License. 14 */ 15 package auth 16 17 import ( 18 "strconv" 19 "strings" 20 ) 21 22 // Scheme define the authentication method. 23 type Scheme byte 24 25 const ( 26 // SchemeUnknown represents unknown or unsupported schemes 27 SchemeUnknown Scheme = iota 28 29 // SchemeBasic represents the "Basic" HTTP authentication scheme. 30 // Reference: https://tools.ietf.org/html/rfc7617 31 SchemeBasic 32 33 // SchemeBearer represents the Bearer token in OAuth 2.0. 34 // Reference: https://tools.ietf.org/html/rfc6750 35 SchemeBearer 36 ) 37 38 // parseScheme parse the authentication scheme from the given string 39 // case-insensitively. 40 func parseScheme(scheme string) Scheme { 41 switch { 42 case strings.EqualFold(scheme, "basic"): 43 return SchemeBasic 44 case strings.EqualFold(scheme, "bearer"): 45 return SchemeBearer 46 } 47 return SchemeUnknown 48 } 49 50 // String return the string for the scheme. 51 func (s Scheme) String() string { 52 switch s { 53 case SchemeBasic: 54 return "Basic" 55 case SchemeBearer: 56 return "Bearer" 57 } 58 return "Unknown" 59 } 60 61 // parseChallenge parses the "WWW-Authenticate" header returned by the remote 62 // registry, and extracts parameters if scheme is Bearer. 63 // References: 64 // - https://docs.docker.com/registry/spec/auth/token/#how-to-authenticate 65 // - https://tools.ietf.org/html/rfc7235#section-2.1 66 func parseChallenge(header string) (scheme Scheme, params map[string]string) { 67 // as defined in RFC 7235 section 2.1, we have 68 // challenge = auth-scheme [ 1*SP ( token68 / #auth-param ) ] 69 // auth-scheme = token 70 // auth-param = token BWS "=" BWS ( token / quoted-string ) 71 // 72 // since we focus parameters only on Bearer, we have 73 // challenge = auth-scheme [ 1*SP #auth-param ] 74 schemeString, rest := parseToken(header) 75 scheme = parseScheme(schemeString) 76 77 // fast path for non bearer challenge 78 if scheme != SchemeBearer { 79 return 80 } 81 82 // parse params for bearer auth. 83 // combining RFC 7235 section 2.1 with RFC 7230 section 7, we have 84 // #auth-param => auth-param *( OWS "," OWS auth-param ) 85 var key, value string 86 for { 87 key, rest = parseToken(skipSpace(rest)) 88 if key == "" { 89 return 90 } 91 92 rest = skipSpace(rest) 93 if rest == "" || rest[0] != '=' { 94 return 95 } 96 rest = skipSpace(rest[1:]) 97 if rest == "" { 98 return 99 } 100 101 if rest[0] == '"' { 102 prefix, err := strconv.QuotedPrefix(rest) 103 if err != nil { 104 return 105 } 106 value, err = strconv.Unquote(prefix) 107 if err != nil { 108 return 109 } 110 rest = rest[len(prefix):] 111 } else { 112 value, rest = parseToken(rest) 113 if value == "" { 114 return 115 } 116 } 117 if params == nil { 118 params = map[string]string{ 119 key: value, 120 } 121 } else { 122 params[key] = value 123 } 124 125 rest = skipSpace(rest) 126 if rest == "" || rest[0] != ',' { 127 return 128 } 129 rest = rest[1:] 130 } 131 } 132 133 // isNotTokenChar reports whether rune is not a `tchar` defined in RFC 7230 134 // section 3.2.6. 135 func isNotTokenChar(r rune) bool { 136 // tchar = "!" / "#" / "$" / "%" / "&" / "'" / "*" 137 // / "+" / "-" / "." / "^" / "_" / "`" / "|" / "~" 138 // / DIGIT / ALPHA 139 // ; any VCHAR, except delimiters 140 return (r < 'A' || r > 'Z') && (r < 'a' || r > 'z') && 141 (r < '0' || r > '9') && !strings.ContainsRune("!#$%&'*+-.^_`|~", r) 142 } 143 144 // parseToken finds the next token from the given string. If no token found, 145 // an empty token is returned and the whole of the input is returned in rest. 146 // Note: Since token = 1*tchar, empty string is not a valid token. 147 func parseToken(s string) (token, rest string) { 148 if i := strings.IndexFunc(s, isNotTokenChar); i != -1 { 149 return s[:i], s[i:] 150 } 151 return s, "" 152 } 153 154 // skipSpace skips "bad" whitespace (BWS) defined in RFC 7230 section 3.2.3. 155 func skipSpace(s string) string { 156 // OWS = *( SP / HTAB ) 157 // ; optional whitespace 158 // BWS = OWS 159 // ; "bad" whitespace 160 if i := strings.IndexFunc(s, func(r rune) bool { 161 return r != ' ' && r != '\t' 162 }); i != -1 { 163 return s[i:] 164 } 165 return s 166 } 167