| OLD | NEW |
| 1 // Copyright 2013 The Chromium Authors. All rights reserved. | 1 // Copyright 2013 The Chromium Authors. All rights reserved. |
| 2 // Use of this source code is governed by a BSD-style license that can be | 2 // Use of this source code is governed by a BSD-style license that can be |
| 3 // found in the LICENSE file. | 3 // found in the LICENSE file. |
| 4 | 4 |
| 5 #include "components/signin/core/browser/signin_header_helper.h" | 5 #include "components/signin/core/browser/signin_header_helper.h" |
| 6 | 6 |
| 7 #include <stddef.h> | 7 #include <stddef.h> |
| 8 #include <map> |
| 8 | 9 |
| 10 #include "base/logging.h" |
| 9 #include "base/macros.h" | 11 #include "base/macros.h" |
| 10 #include "base/strings/string_number_conversions.h" | |
| 11 #include "base/strings/string_split.h" | 12 #include "base/strings/string_split.h" |
| 12 #include "base/strings/string_util.h" | |
| 13 #include "base/strings/stringprintf.h" | |
| 14 #include "build/build_config.h" | 13 #include "build/build_config.h" |
| 15 #include "components/content_settings/core/browser/cookie_settings.h" | 14 #include "components/content_settings/core/browser/cookie_settings.h" |
| 16 #include "components/google/core/browser/google_util.h" | 15 #include "components/google/core/browser/google_util.h" |
| 16 #include "components/signin/core/browser/chrome_connected_header_helper.h" |
| 17 #include "components/signin/core/common/profile_management_switches.h" | 17 #include "components/signin/core/common/profile_management_switches.h" |
| 18 #include "google_apis/gaia/gaia_auth_util.h" | 18 #include "google_apis/gaia/gaia_auth_util.h" |
| 19 #include "google_apis/gaia/gaia_urls.h" | 19 #include "google_apis/gaia/gaia_urls.h" |
| 20 #include "net/base/escape.h" | 20 #include "net/base/escape.h" |
| 21 #include "net/base/registry_controlled_domains/registry_controlled_domain.h" | |
| 22 #include "net/http/http_response_headers.h" | 21 #include "net/http/http_response_headers.h" |
| 23 #include "net/url_request/url_request.h" | 22 #include "net/url_request/url_request.h" |
| 24 #include "url/gurl.h" | 23 #include "url/gurl.h" |
| 25 | 24 |
| 25 #if !defined(OS_IOS) && !defined(OS_ANDROID) |
| 26 #include "components/signin/core/browser/dice_header_helper.h" |
| 27 #endif |
| 28 |
| 26 namespace signin { | 29 namespace signin { |
| 27 | 30 |
| 28 namespace { | 31 namespace { |
| 29 | 32 |
| 30 // Dictionary of fields in a mirror response header. | 33 // Dictionary of fields in a mirror response header. |
| 31 typedef std::map<std::string, std::string> MirrorResponseHeaderDictionary; | 34 typedef std::map<std::string, std::string> MirrorResponseHeaderDictionary; |
| 32 | 35 |
| 33 const char kChromeManageAccountsHeader[] = "X-Chrome-Manage-Accounts"; | 36 const char kChromeManageAccountsHeader[] = "X-Chrome-Manage-Accounts"; |
| 34 const char kContinueUrlAttrName[] = "continue_url"; | 37 const char kContinueUrlAttrName[] = "continue_url"; |
| 35 const char kEmailAttrName[] = "email"; | 38 const char kEmailAttrName[] = "email"; |
| 36 const char kEnableAccountConsistencyAttrName[] = "enable_account_consistency"; | |
| 37 const char kGaiaIdAttrName[] = "id"; | |
| 38 const char kProfileModeAttrName[] = "mode"; | |
| 39 const char kIsSameTabAttrName[] = "is_same_tab"; | 39 const char kIsSameTabAttrName[] = "is_same_tab"; |
| 40 const char kIsSamlAttrName[] = "is_saml"; | 40 const char kIsSamlAttrName[] = "is_saml"; |
| 41 const char kServiceTypeAttrName[] = "action"; | 41 const char kServiceTypeAttrName[] = "action"; |
| 42 | 42 |
| 43 bool IsDriveOrigin(const GURL& url) { | 43 // Determines the service type that has been passed from Gaia in the header. |
| 44 if (!url.SchemeIsCryptographic()) | |
| 45 return false; | |
| 46 | |
| 47 const GURL kGoogleDriveURL("https://drive.google.com"); | |
| 48 const GURL kGoogleDocsURL("https://docs.google.com"); | |
| 49 return url == kGoogleDriveURL || url == kGoogleDocsURL; | |
| 50 } | |
| 51 | |
| 52 bool IsUrlEligibleToIncludeGaiaId(const GURL& url, bool is_header_request) { | |
| 53 if (is_header_request) { | |
| 54 // GAIA Id is only necessary for Drive. Don't set it otherwise. | |
| 55 return IsDriveOrigin(url.GetOrigin()); | |
| 56 } | |
| 57 | |
| 58 // Cookie requests don't have the granularity to only include the GAIA Id for | |
| 59 // Drive origin. Set it on all google.com instead. | |
| 60 if (!url.SchemeIsCryptographic()) | |
| 61 return false; | |
| 62 | |
| 63 const std::string kGoogleDomain = "google.com"; | |
| 64 std::string domain = net::registry_controlled_domains::GetDomainAndRegistry( | |
| 65 url, net::registry_controlled_domains::EXCLUDE_PRIVATE_REGISTRIES); | |
| 66 return domain == kGoogleDomain; | |
| 67 } | |
| 68 | |
| 69 // Determines the service type that has been passed from GAIA in the header. | |
| 70 GAIAServiceType GetGAIAServiceTypeFromHeader(const std::string& header_value) { | 44 GAIAServiceType GetGAIAServiceTypeFromHeader(const std::string& header_value) { |
| 71 if (header_value == "SIGNOUT") | 45 if (header_value == "SIGNOUT") |
| 72 return GAIA_SERVICE_TYPE_SIGNOUT; | 46 return GAIA_SERVICE_TYPE_SIGNOUT; |
| 73 else if (header_value == "INCOGNITO") | 47 else if (header_value == "INCOGNITO") |
| 74 return GAIA_SERVICE_TYPE_INCOGNITO; | 48 return GAIA_SERVICE_TYPE_INCOGNITO; |
| 75 else if (header_value == "ADDSESSION") | 49 else if (header_value == "ADDSESSION") |
| 76 return GAIA_SERVICE_TYPE_ADDSESSION; | 50 return GAIA_SERVICE_TYPE_ADDSESSION; |
| 77 else if (header_value == "REAUTH") | 51 else if (header_value == "REAUTH") |
| 78 return GAIA_SERVICE_TYPE_REAUTH; | 52 return GAIA_SERVICE_TYPE_REAUTH; |
| 79 else if (header_value == "SIGNUP") | 53 else if (header_value == "SIGNUP") |
| 80 return GAIA_SERVICE_TYPE_SIGNUP; | 54 return GAIA_SERVICE_TYPE_SIGNUP; |
| 81 else if (header_value == "DEFAULT") | 55 else if (header_value == "DEFAULT") |
| 82 return GAIA_SERVICE_TYPE_DEFAULT; | 56 return GAIA_SERVICE_TYPE_DEFAULT; |
| 83 else | 57 else |
| 84 return GAIA_SERVICE_TYPE_NONE; | 58 return GAIA_SERVICE_TYPE_NONE; |
| 85 } | 59 } |
| 86 | 60 |
| 87 // Parses the mirror response header. Its expected format is | 61 // Parses the mirror response header. Its expected format is |
| 88 // "key1=value1,key2=value2,...". | 62 // "key1=value1,key2=value2,...". |
| 89 MirrorResponseHeaderDictionary ParseMirrorResponseHeader( | 63 MirrorResponseHeaderDictionary ParseMirrorResponseHeader( |
| 90 const std::string& header_value) { | 64 const std::string& header_value) { |
| 91 MirrorResponseHeaderDictionary dictionary; | 65 MirrorResponseHeaderDictionary dictionary; |
| 92 for (const base::StringPiece& field : | 66 for (const base::StringPiece& field : |
| 93 base::SplitStringPiece(header_value, ",", base::KEEP_WHITESPACE, | 67 base::SplitStringPiece(header_value, ",", base::KEEP_WHITESPACE, |
| 94 base::SPLIT_WANT_NONEMPTY)) { | 68 base::SPLIT_WANT_NONEMPTY)) { |
| 95 size_t delim = field.find_first_of('='); | 69 size_t delim = field.find_first_of('='); |
| 96 if (delim == std::string::npos) { | 70 if (delim == std::string::npos) { |
| 97 DLOG(WARNING) << "Unexpected GAIA header field '" << field << "'."; | 71 DLOG(WARNING) << "Unexpected Gaia header field '" << field << "'."; |
| 98 continue; | 72 continue; |
| 99 } | 73 } |
| 100 dictionary[field.substr(0, delim).as_string()] = net::UnescapeURLComponent( | 74 dictionary[field.substr(0, delim).as_string()] = net::UnescapeURLComponent( |
| 101 field.substr(delim + 1).as_string(), | 75 field.substr(delim + 1).as_string(), |
| 102 net::UnescapeRule::PATH_SEPARATORS | | 76 net::UnescapeRule::PATH_SEPARATORS | |
| 103 net::UnescapeRule::URL_SPECIAL_CHARS_EXCEPT_PATH_SEPARATORS); | 77 net::UnescapeRule::URL_SPECIAL_CHARS_EXCEPT_PATH_SEPARATORS); |
| 104 } | 78 } |
| 105 return dictionary; | 79 return dictionary; |
| 106 } | 80 } |
| 107 | 81 |
| 108 // Checks if the url has the required properties to have a X-Chrome-Connected | |
| 109 // header. | |
| 110 bool IsUrlEligibleForXChromeConnectedHeader(const GURL& url) { | |
| 111 // Only set the header for Drive and Gaia always, and other Google properties | |
| 112 // if account consistency is enabled. | |
| 113 // Vasquette, which is integrated with most Google properties, needs the | |
| 114 // header to redirect certain user actions to Chrome native UI. Drive and Gaia | |
| 115 // need the header to tell if the current user is connected. The drive path is | |
| 116 // a temporary workaround until the more generic chrome.principals API is | |
| 117 // available. | |
| 118 | |
| 119 // Consider the account id sensitive and limit it to secure domains. | |
| 120 if (!url.SchemeIsCryptographic()) | |
| 121 return false; | |
| 122 | |
| 123 GURL origin(url.GetOrigin()); | |
| 124 bool is_enable_account_consistency = | |
| 125 switches::IsAccountConsistencyMirrorEnabled(); | |
| 126 bool is_google_url = is_enable_account_consistency && | |
| 127 (google_util::IsGoogleDomainUrl( | |
| 128 url, google_util::ALLOW_SUBDOMAIN, | |
| 129 google_util::DISALLOW_NON_STANDARD_PORTS) || | |
| 130 google_util::IsYoutubeDomainUrl( | |
| 131 url, google_util::ALLOW_SUBDOMAIN, | |
| 132 google_util::DISALLOW_NON_STANDARD_PORTS)); | |
| 133 return is_google_url || IsDriveOrigin(origin) || | |
| 134 gaia::IsGaiaSignonRealm(origin); | |
| 135 } | |
| 136 | |
| 137 // Checks if the url has the required properties to have an account consistency | |
| 138 // header. | |
| 139 bool IsUrlEligibleForAccountConsistencyRequestHeader(const GURL& url) { | |
| 140 // TODO(droger): Support X-Chrome-ID-Consistency-Request. | |
| 141 return IsUrlEligibleForXChromeConnectedHeader(url); | |
| 142 } | |
| 143 | |
| 144 std::string BuildMirrorRequestIfPossible( | |
| 145 bool is_header_request, | |
| 146 const GURL& url, | |
| 147 const std::string& account_id, | |
| 148 const content_settings::CookieSettings* cookie_settings, | |
| 149 int profile_mode_mask) { | |
| 150 if (account_id.empty()) | |
| 151 return std::string(); | |
| 152 | |
| 153 // If signin cookies are not allowed, don't add the header. | |
| 154 if (!SettingsAllowSigninCookies(cookie_settings)) { | |
| 155 return std::string(); | |
| 156 } | |
| 157 | |
| 158 // Check if url is elligible for the header. | |
| 159 if (!IsUrlEligibleForXChromeConnectedHeader(url)) | |
| 160 return std::string(); | |
| 161 | |
| 162 std::vector<std::string> parts; | |
| 163 if (IsUrlEligibleToIncludeGaiaId(url, is_header_request)) { | |
| 164 // Only set the GAIA Id on domains that actually requires it. | |
| 165 parts.push_back( | |
| 166 base::StringPrintf("%s=%s", kGaiaIdAttrName, account_id.c_str())); | |
| 167 } | |
| 168 parts.push_back( | |
| 169 base::StringPrintf("%s=%s", kProfileModeAttrName, | |
| 170 base::IntToString(profile_mode_mask).c_str())); | |
| 171 parts.push_back(base::StringPrintf( | |
| 172 "%s=%s", kEnableAccountConsistencyAttrName, | |
| 173 switches::IsAccountConsistencyMirrorEnabled() ? "true" : "false")); | |
| 174 | |
| 175 return base::JoinString(parts, is_header_request ? "," : ":"); | |
| 176 } | |
| 177 | |
| 178 } // namespace | 82 } // namespace |
| 179 | 83 |
| 180 extern const char kChromeConnectedHeader[] = "X-Chrome-Connected"; | 84 extern const char kChromeConnectedHeader[] = "X-Chrome-Connected"; |
| 85 extern const char kDiceRequestHeader[] = "X-Chrome-ID-Consistency-Request"; |
| 181 | 86 |
| 182 ManageAccountsParams::ManageAccountsParams() | 87 ManageAccountsParams::ManageAccountsParams() |
| 183 : service_type(GAIA_SERVICE_TYPE_NONE), | 88 : service_type(GAIA_SERVICE_TYPE_NONE), |
| 184 email(""), | 89 email(""), |
| 185 is_saml(false), | 90 is_saml(false), |
| 186 continue_url(""), | 91 continue_url(""), |
| 187 is_same_tab(false) { | 92 is_same_tab(false) { |
| 188 #if !defined(OS_IOS) | 93 #if !defined(OS_IOS) |
| 189 child_id = 0; | 94 child_id = 0; |
| 190 route_id = 0; | 95 route_id = 0; |
| (...skipping 10 matching lines...) Expand all Loading... |
| 201 return cookie_settings && | 106 return cookie_settings && |
| 202 cookie_settings->IsCookieAccessAllowed(gaia_url, gaia_url) && | 107 cookie_settings->IsCookieAccessAllowed(gaia_url, gaia_url) && |
| 203 cookie_settings->IsCookieAccessAllowed(google_url, google_url); | 108 cookie_settings->IsCookieAccessAllowed(google_url, google_url); |
| 204 } | 109 } |
| 205 | 110 |
| 206 std::string BuildMirrorRequestCookieIfPossible( | 111 std::string BuildMirrorRequestCookieIfPossible( |
| 207 const GURL& url, | 112 const GURL& url, |
| 208 const std::string& account_id, | 113 const std::string& account_id, |
| 209 const content_settings::CookieSettings* cookie_settings, | 114 const content_settings::CookieSettings* cookie_settings, |
| 210 int profile_mode_mask) { | 115 int profile_mode_mask) { |
| 211 return BuildMirrorRequestIfPossible(false /* is_header_request */, url, | 116 return signin::ChromeConnectedHeaderHelper::BuildRequestCookieIfPossible( |
| 212 account_id, cookie_settings, | 117 url, account_id, cookie_settings, profile_mode_mask); |
| 213 profile_mode_mask); | |
| 214 } | 118 } |
| 215 | 119 |
| 216 bool AppendOrRemoveAccountConsistentyRequestHeader( | 120 bool SigninHeaderHelper::AppendOrRemoveRequestHeader( |
| 217 net::URLRequest* request, | 121 net::URLRequest* request, |
| 122 const char* header_name, |
| 218 const GURL& redirect_url, | 123 const GURL& redirect_url, |
| 219 const std::string& account_id, | 124 const std::string& account_id, |
| 220 const content_settings::CookieSettings* cookie_settings, | 125 const content_settings::CookieSettings* cookie_settings, |
| 221 int profile_mode_mask) { | 126 int profile_mode_mask) { |
| 222 const GURL& url = redirect_url.is_empty() ? request->url() : redirect_url; | 127 const GURL& url = redirect_url.is_empty() ? request->url() : redirect_url; |
| 223 | 128 std::string header_value = BuildRequestHeaderIfPossible( |
| 224 // TODO(droger): Support X-Chrome-ID-Consistency-Request. | |
| 225 std::string header_name = kChromeConnectedHeader; | |
| 226 std::string header_value = BuildMirrorRequestIfPossible( | |
| 227 true /* is_header_request */, url, account_id, cookie_settings, | 129 true /* is_header_request */, url, account_id, cookie_settings, |
| 228 profile_mode_mask); | 130 profile_mode_mask); |
| 229 | 131 |
| 230 if (!header_name.empty() && header_value.empty()) { | 132 if (header_value.empty()) { |
| 231 // If the request is being redirected, and it has the account consistency | 133 // If the request is being redirected, and it has the account consistency |
| 232 // header, and current url is a Google URL, and the redirected one is not, | 134 // header, and current url is a Google URL, and the redirected one is not, |
| 233 // remove the header. | 135 // remove the header. |
| 234 if (!redirect_url.is_empty() && | 136 if (!redirect_url.is_empty() && |
| 235 request->extra_request_headers().HasHeader(header_name) && | 137 request->extra_request_headers().HasHeader(header_name) && |
| 236 IsUrlEligibleForAccountConsistencyRequestHeader(request->url()) && | 138 IsUrlEligibleForRequestHeader(request->url()) && |
| 237 !IsUrlEligibleForAccountConsistencyRequestHeader(redirect_url)) { | 139 !IsUrlEligibleForRequestHeader(redirect_url)) { |
| 238 request->RemoveRequestHeaderByName(header_name); | 140 request->RemoveRequestHeaderByName(header_name); |
| 239 } | 141 } |
| 240 return false; | 142 return false; |
| 241 } | 143 } |
| 242 request->SetExtraRequestHeaderByName(header_name, header_value, false); | 144 request->SetExtraRequestHeaderByName(header_name, header_value, false); |
| 243 return true; | 145 return true; |
| 244 } | 146 } |
| 245 | 147 |
| 148 std::string SigninHeaderHelper::BuildRequestHeaderIfPossible( |
| 149 bool is_header_request, |
| 150 const GURL& url, |
| 151 const std::string& account_id, |
| 152 const content_settings::CookieSettings* cookie_settings, |
| 153 int profile_mode_mask) { |
| 154 // If signin cookies are not allowed, don't add the header. |
| 155 if (!SettingsAllowSigninCookies(cookie_settings)) |
| 156 return std::string(); |
| 157 |
| 158 // Check if url is eligible for the header. |
| 159 if (!IsUrlEligibleForRequestHeader(url)) |
| 160 return std::string(); |
| 161 |
| 162 return BuildRequestHeader(is_header_request, url, account_id, |
| 163 profile_mode_mask); |
| 164 } |
| 165 |
| 166 void AppendOrRemoveAccountConsistentyRequestHeader( |
| 167 net::URLRequest* request, |
| 168 const GURL& redirect_url, |
| 169 const std::string& account_id, |
| 170 const content_settings::CookieSettings* cookie_settings, |
| 171 int profile_mode_mask) { |
| 172 // Dice is not enabled on mobile. |
| 173 #if !defined(OS_IOS) && !defined(OS_ANDROID) |
| 174 DiceHeaderHelper dice_helper; |
| 175 dice_helper.AppendOrRemoveRequestHeader(request, kDiceRequestHeader, |
| 176 redirect_url, account_id, |
| 177 cookie_settings, profile_mode_mask); |
| 178 #endif |
| 179 |
| 180 ChromeConnectedHeaderHelper chrome_connected_helper; |
| 181 chrome_connected_helper.AppendOrRemoveRequestHeader( |
| 182 request, kChromeConnectedHeader, redirect_url, account_id, |
| 183 cookie_settings, profile_mode_mask); |
| 184 } |
| 185 |
| 246 ManageAccountsParams BuildManageAccountsParams( | 186 ManageAccountsParams BuildManageAccountsParams( |
| 247 const std::string& header_value) { | 187 const std::string& header_value) { |
| 248 ManageAccountsParams params; | 188 ManageAccountsParams params; |
| 249 MirrorResponseHeaderDictionary header_dictionary = | 189 MirrorResponseHeaderDictionary header_dictionary = |
| 250 ParseMirrorResponseHeader(header_value); | 190 ParseMirrorResponseHeader(header_value); |
| 251 MirrorResponseHeaderDictionary::const_iterator it = header_dictionary.begin(); | 191 MirrorResponseHeaderDictionary::const_iterator it = header_dictionary.begin(); |
| 252 for (; it != header_dictionary.end(); ++it) { | 192 for (; it != header_dictionary.end(); ++it) { |
| 253 const std::string key_name(it->first); | 193 const std::string key_name(it->first); |
| 254 if (key_name == kServiceTypeAttrName) { | 194 if (key_name == kServiceTypeAttrName) { |
| 255 params.service_type = | 195 params.service_type = |
| 256 GetGAIAServiceTypeFromHeader(header_dictionary[kServiceTypeAttrName]); | 196 GetGAIAServiceTypeFromHeader(header_dictionary[kServiceTypeAttrName]); |
| 257 } else if (key_name == kEmailAttrName) { | 197 } else if (key_name == kEmailAttrName) { |
| 258 params.email = header_dictionary[kEmailAttrName]; | 198 params.email = header_dictionary[kEmailAttrName]; |
| 259 } else if (key_name == kIsSamlAttrName) { | 199 } else if (key_name == kIsSamlAttrName) { |
| 260 params.is_saml = header_dictionary[kIsSamlAttrName] == "true"; | 200 params.is_saml = header_dictionary[kIsSamlAttrName] == "true"; |
| 261 } else if (key_name == kContinueUrlAttrName) { | 201 } else if (key_name == kContinueUrlAttrName) { |
| 262 params.continue_url = header_dictionary[kContinueUrlAttrName]; | 202 params.continue_url = header_dictionary[kContinueUrlAttrName]; |
| 263 } else if (key_name == kIsSameTabAttrName) { | 203 } else if (key_name == kIsSameTabAttrName) { |
| 264 params.is_same_tab = header_dictionary[kIsSameTabAttrName] == "true"; | 204 params.is_same_tab = header_dictionary[kIsSameTabAttrName] == "true"; |
| 265 } else { | 205 } else { |
| 266 DLOG(WARNING) << "Unexpected GAIA header attribute '" << key_name << "'."; | 206 DLOG(WARNING) << "Unexpected Gaia header attribute '" << key_name << "'."; |
| 267 } | 207 } |
| 268 } | 208 } |
| 269 return params; | 209 return params; |
| 270 } | 210 } |
| 271 | 211 |
| 272 ManageAccountsParams BuildManageAccountsParamsIfExists(net::URLRequest* request, | 212 ManageAccountsParams BuildManageAccountsParamsIfExists(net::URLRequest* request, |
| 273 bool is_off_the_record) { | 213 bool is_off_the_record) { |
| 274 ManageAccountsParams empty_params; | 214 ManageAccountsParams empty_params; |
| 275 empty_params.service_type = GAIA_SERVICE_TYPE_NONE; | 215 empty_params.service_type = GAIA_SERVICE_TYPE_NONE; |
| 276 if (!gaia::IsGaiaSignonRealm(request->url().GetOrigin())) | 216 if (!gaia::IsGaiaSignonRealm(request->url().GetOrigin())) |
| 277 return empty_params; | 217 return empty_params; |
| 278 | 218 |
| 279 std::string header_value; | 219 std::string header_value; |
| 280 net::HttpResponseHeaders* response_headers = request->response_headers(); | 220 net::HttpResponseHeaders* response_headers = request->response_headers(); |
| 281 if (!response_headers || | 221 if (!response_headers || |
| 282 !response_headers->GetNormalizedHeader( | 222 !response_headers->GetNormalizedHeader( |
| 283 kChromeManageAccountsHeader, &header_value)) { | 223 kChromeManageAccountsHeader, &header_value)) { |
| 284 return empty_params; | 224 return empty_params; |
| 285 } | 225 } |
| 286 | 226 |
| 287 DCHECK(switches::IsAccountConsistencyMirrorEnabled() && !is_off_the_record); | 227 DCHECK(switches::IsAccountConsistencyMirrorEnabled() && !is_off_the_record); |
| 288 return BuildManageAccountsParams(header_value); | 228 return BuildManageAccountsParams(header_value); |
| 289 } | 229 } |
| 290 | 230 |
| 291 } // namespace signin | 231 } // namespace signin |
| OLD | NEW |