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