Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Angular2 Http missed "Authentication" header in CORS preflight actual (second) request #13554

Closed
jing-zhou opened this issue Dec 18, 2016 · 13 comments
Labels
needs reproduction This issue needs a reproduction in order for the team to investigate further

Comments

@jing-zhou
Copy link

I'm submitting a ... (check one with "x")

[x] bug report => search github for a similar issue or PR before submitting
[ ] feature request
[ ] support request => Please do not submit support request here, instead see https://github.com/angular/angular/blob/master/CONTRIBUTING.md#question

Current behavior

Http missed the "Authentication" header in the actual (second) request of CORS preflight session. please see
https://forum.ionicframework.com/t/cr3-http-missed-authorization-header-in-cors-preflight-actual-request/73630
Expected behavior

Http should insert the "Authorization" header in the actual POST message headers after the success of "OPTIONS" round in a CORS preflight scenario
Minimal reproduction of the problem with instructions

set up a Restful service with CORS capacity; send a request with a "Authentication: Bear" token in the message; then try to extract the "Authentication" header from the received message at the server side
What is the motivation / use case for changing the behavior?

Please tell us about your environment:

Ionic2 RC3 -- emulated on Ubuntu 16.04 LTS Linux 4.4

  • Angular version: 2.1.1
  • Browser: [ Chromium 53.0.2785.143 Built on Ubuntu ]
  • Language: [TypeScript 2.0.3 ]

  • Node (for AoT issues): node --version = v4.6.2

@DzmitryShylovich
Copy link
Contributor

can u make the same request using plain hxr/fetch and compare results?

@jing-zhou
Copy link
Author

jing-zhou commented Dec 19, 2016

Hello,
I am using a npm package client-oauth2 which is based on XMLHttpRequest and Promise; this package provided the "password" grantType which mean you can get an Oauth2 token by providing username and password.

In order to get an Oauth2 token by "password" grantType, you will also have to provide "ClientId" and "ClientSecret" which was implemented as "Authorization: Basic" header into a request message headers.
I am pasting here the message sequence of the corresponding CORS preflight session for your reference

the "OPTIONS" round

OPTIONS /oauth2/token HTTP/1.1
Host: is.gubnoi.com:9443
Connection: keep-alive
Access-Control-Request-Method: POST
Origin: http://localhost:8100
User-Agent: Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/46.0.2490.76 Mobile Safari/537.36
Access-Control-Request-Headers: authorization
Accept: */*
Referer: http://localhost:8100/?ionicplatform=ios
Accept-Encoding: gzip, deflate, sdch, br
Accept-Language: en-US,en;q=0.8,de;q=0.6,es;q=0.4,fr;q=0.2,zh-CN;q=0.2,zh-TW;q=0.2

HTTP/1.1 200 OK
X-Frame-Options: DENY
X-Content-Type-Options: nosniff
X-XSS-Protection: 1; mode=block
Access-Control-Allow-Origin: http://localhost:8100
Access-Control-Allow-Credentials: true
Access-Control-Max-Age: 1209600
Access-Control-Allow-Methods: GET, OPTIONS, HEAD, POST
Access-Control-Allow-Headers: Accept-Charset, Authorization, Origin, Accept, User-Agent, Accept-Encoding, Accept-Language, Content-Length, Accept-Datetime, Content-Type
Content-Length: 0
Date: Mon, 19 Dec 2016 02:16:56 GMT
Server: WSO2 Carbon Server

the "POST" round

POST /oauth2/token HTTP/1.1
Host: is.gubnoi.com:9443
Connection: keep-alive
Content-Length: 55
Accept: application/json, application/x-www-form-urlencoded
Origin: http://localhost:8100
User-Agent: Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/46.0.2490.76 Mobile Safari/537.36
Authorization: Basic SjNsYk1NTUpGd1hCNm5lS3pYdjAzMFM5bGZnYTpyczZNSVU0MTVVRjZrZ2ZBZnFmQVA4MGNFY0Fh
Content-Type: application/x-www-form-urlencoded
Referer: http://localhost:8100/?ionicplatform=ios
Accept-Encoding: gzip, deflate, br
Accept-Language: en-US,en;q=0.8,de;q=0.6,es;q=0.4,fr;q=0.2,zh-CN;q=0.2,zh-TW;q=0.2

HTTP/1.1 200 OK
X-Frame-Options: DENY
X-Content-Type-Options: nosniff
X-XSS-Protection: 1; mode=block
Access-Control-Allow-Origin: http://localhost:8100
Access-Control-Allow-Credentials: true
Access-Control-Expose-Headers: Accept-Charset, Authorization, Origin, Accept, User-Agent, Accept-Encoding, Accept-Language, Content-Length, Accept-Datetime, Content-Type
Cache-Control: no-store
Date: Mon, 19 Dec 2016 02:17:07 GMT
Pragma: no-cache
Content-Type: application/json
Content-Length: 553
Server: WSO2 Carbon Server

// the parsed message body, I have cut access token for clearance 
access_token: "eyJhbGciOiJSUzI1NiJ9.eyJpc3Mi..."
expires_in: 3600
refresh_token: "d2a0d334-e0e8-31aa-a000-a84691d17e12"
token_type: "Bearer"

@jing-zhou
Copy link
Author

jing-zhou commented Dec 19, 2016

I realize that I have had a typo in the original issue title: the "Authentication" should be "Authorization"

@DzmitryShylovich
Copy link
Contributor

@jing-zhou hello. it's still not very helpful without reproduction.

asking again. have u tried to make the same request using plain xhr? Angular doesn't do anything with headers.

@jing-zhou
Copy link
Author

well, I have to admit that I am not quite familiar with xhr. I am not directly using the xhr myself.

but as I said, I have been using a npm package (client-oauth2) that is based on xhr. below are excerpts of my implements and that of the package till the final XMLHttpRequest call. and the result was a success as you can see from the above message print-out

the initial calling from the web

ionViewWillEnter(){
   this.lf = new FormGroup({    
        // Supports alphabets and numbers no special characters except underscore('_') and dash('-') min 3 and max 20 characters. 
        userName: new FormControl('', [Validators.required, Validators.pattern('^[A-Za-z0-9_-]{3,20}$')]), 
       //Password supports special characters and underscore('_') and dash('-') here min length 6 max 20 charters.
        password: new FormControl('', [Validators.required,Validators.pattern('^[A-Za-z0-9!@#$%^&*()_-]{6,20}$')]),
        
    })
  }

  public login(){
    
    Oauth2.fetch(this.lf.get("userName").value,this.lf.get("password").value );
    debugger;
  }

the provider implementation:

public static fetch(user: string, pass: string): boolean{
    let ret = true;

    Oauth2.oauth2.owner.getToken(user, pass)
      .then((tk) => {Oauth2._tk = tk;
                     User.anonymous = false;},
             () => {ret = false;});
    debugger;
    return ret;
  }

the implementation of the getToken()

/**
 * Make a request on behalf of the user credentials to get an acces token.
 *
 * @param  {String}  username
 * @param  {String}  password
 * @return {Promise}
 */
OwnerFlow.prototype.getToken = function (username, password, options) {
  var self = this

  options = extend(this.client.options, options)

  return this.client._request(requestOptions({
    url: options.accessTokenUri,
    method: 'POST',
    headers: extend(DEFAULT_HEADERS, {
      Authorization: auth(options.clientId, options.clientSecret)
    }),
    body: {
      scope: sanitizeScope(options.scopes),
      username: username,
      password: password,
      grant_type: 'password'
    }
  }, options))
    .then(function (data) {
      return self.client.createToken(data)
    })
}

the implementation of _request()

/**
 * Using the built-in request method, we'll automatically attempt to parse
 * the response.
 *
 * @param  {Object}  options
 * @return {Promise}
 */
ClientOAuth2.prototype._request = function (options) {
  var url = options.url
  var body = Querystring.stringify(options.body)
  var query = Querystring.stringify(options.query)

  if (query) {
    url += (url.indexOf('?') === -1 ? '?' : '&') + query
  }

  return this.request(options.method, url, body, options.headers)
    .then(function (res) {
      var body = parseResponseBody(res.body)
      var authErr = getAuthError(body)

      if (authErr) {
        return Promise.reject(authErr)
      }

      if (res.status < 200 || res.status >= 399) {
        var statusErr = new Error('HTTP status ' + res.status)
        statusErr.status = res.status
        statusErr.body = res.body
        statusErr.code = 'ESTATUS'
        return Promise.reject(statusErr)
      }

      return body
    })
}

the implementation of request(), and the final call to xhr

/**
 * Make a request using `XMLHttpRequest`.
 *
 * @param   {String}  method
 * @param   {String}  url
 * @param   {String}  body
 * @param   {Object}  headers
 * @returns {Promise}
 */
module.exports = function request (method, url, body, headers) {
  return new Promise(function (resolve, reject) {
    var xhr = new window.XMLHttpRequest()

    xhr.open(method, url)

    xhr.onload = function () {
      return resolve({
        status: xhr.status,
        body: xhr.responseText
      })
    }

    xhr.onerror = xhr.onabort = function () {
      return reject(new Error(xhr.statusText || 'XHR aborted: ' + url))
    }

    Object.keys(headers).forEach(function (header) {
      xhr.setRequestHeader(header, headers[header])
    })

    xhr.send(body)
  })
}

@jing-zhou
Copy link
Author

and following are my implementation with https

the initial call from webpage

ionViewWillEnter() {
    this.gabriel.display().subscribe(
        data=> { console.log('after gabriel');
          this.profile.userName = data['userName'];
          this.profile.password = data['password'];
          debugger;
        },
        error => {
          console.log('somthing went wrong ');
          debugger;
        },
        () => console.log('left GeneralPage')
    );
  }

the provider implementation of display()

 display():Observable<{}>{
    //only login user can display his /her profile          
    debugger;
    let options = Oauth2.sign(new RequestOptions({ headers: new Headers({'Content-Type': 'application/json'})}));
    let body = JSON.stringify({});
    debugger;
    return this.http.post(Urls.display, body, options).timeout(10000).map((res:Response) => res.json());
  }

the implementation of sign(), which trace back to the previous provider

public static sign(opt: RequestOptions): RequestOptions {
   // the original RequestOptions will be return in the worest scenario
   let res: RequestOptions = opt;
   //the user had signed in and the token expired
   debugger;
   if(Oauth2._tk.expired()){
     debugger;
     Oauth2._tk.refresh()
         .then((tk) => { Oauth2._tk = tk;},  
               // the refresh somehow had failed
               () => { User.anonymous = true;});
   }
   debugger;
   if(!User.anonymous){
     debugger;
     res = Oauth2._tk.sign(opt);
   }  
   debugger;
   return res;
 }

the implementation of sign() in the npm package

/**
 * Sign a standardised request object with user authentication information.
 *
 * @param  {Object} requestObject
 * @return {Object}
 */
ClientOAuth2Token.prototype.sign = function (requestObject) {
  if (!this.accessToken) {
    throw new Error('Unable to sign without access token')
  }

  requestObject.headers = requestObject.headers || {}

  if (this.tokenType === 'bearer') {
    requestObject.headers.Authorization = 'Bearer ' + this.accessToken
  } else {
    var parts = requestObject.url.split('#')
    var token = 'access_token=' + this.accessToken
    var url = parts[0].replace(/[?&]access_token=[^&#]/, '')
    var fragment = parts[1] ? '#' + parts[1] : ''

    // Prepend the correct query string parameter to the url.
    requestObject.url = url + (url.indexOf('?') > -1 ? '&' : '?') + token + fragment

    // Attempt to avoid storing the url in proxies, since the access token
    // is exposed in the query parameters.
    requestObject.headers.Pragma = 'no-store'
    requestObject.headers['Cache-Control'] = 'no-store'
  }

  return requestObject
}

@jing-zhou
Copy link
Author

"Angular doesn't do anything with headers."
well, it is true that Http does not manipulate message headers...

but it is talking with xhr and handling the CORS session which is invisible to a calling client and happens automatically after the call "http.post(...)"

@DzmitryShylovich
Copy link
Contributor

Seems related to #11423

@vicb
Copy link
Contributor

vicb commented Dec 20, 2016

Please add a repro in a plunker. Otherwise we can not help.
Describe clearly the observed vs the desired behavior.

@vicb vicb added flag: can be closed? needs reproduction This issue needs a reproduction in order for the team to investigate further labels Dec 20, 2016
@BafS
Copy link

BafS commented Jan 26, 2017

After hours of search I found a solution. Add "Access-Control-Expose-Headers" : "Authorization" on the server side to expose the Authorization header.

@DzmitryShylovich
Copy link
Contributor

@pkozlowski-opensource can be closed

@subodh6580
Copy link

hi guys any body here i am facing a problem error 422 in Angular 2

@angular-automatic-lock-bot
Copy link

This issue has been automatically locked due to inactivity.
Please file a new issue if you are encountering a similar or related problem.

Read more about our automatic conversation locking policy.

This action has been performed automatically by a bot.

@angular-automatic-lock-bot angular-automatic-lock-bot bot locked and limited conversation to collaborators Sep 13, 2019
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
needs reproduction This issue needs a reproduction in order for the team to investigate further
Projects
None yet
Development

No branches or pull requests

7 participants