openid / AppAuth-JS

JavaScript client SDK for communicating with OAuth 2.0 and OpenID Connect providers.
Apache License 2.0
985 stars 161 forks source link

Module parse failed: Unexpected character '#' (1:0) with Create React App #60

Closed mraible closed 6 years ago

mraible commented 6 years ago

I was able to successfully modify the appauth-js-electron-sample app to log in with my identity provider. Now I'm trying to integrate it into a React + Electron app I created with Create React App. After adding this library and code, I get a strange error when I run npm run build, which runs react-scripts build.

> foo@0.1.0 build /Users/mraible/foo
> react-scripts build

Creating an optimized production build...
Failed to compile.

./node_modules/opener/opener.js
Module parse failed: Unexpected character '#' (1:0)
You may need an appropriate loader to handle this file type.
| #!/usr/bin/env node
|
| "use strict";

error Command failed with exit code 1.

Expected Behavior

My React project still builds just like the example app does.

Describe the problem

Once I add the following AuthService.js to src directory and reference it in another class, the error starts happening. This class is very similar to the sample's flow.ts, except that it's written in JavaScript and supports PKCE.

/*
 * Copyright 2017 Google Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not
 * use this file except in compliance with the License. You may obtain a copy of
 * the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 * License for the specific language governing permissions and limitations under
 * the License.
 */

import { AuthorizationRequest } from '@openid/appauth/built/authorization_request';
import { AuthorizationNotifier } from '@openid/appauth/built/authorization_request_handler';
import { AuthorizationServiceConfiguration } from '@openid/appauth/built/authorization_service_configuration';
import { NodeBasedHandler } from '@openid/appauth/built/node_support/node_request_handler';
import { NodeRequestor } from '@openid/appauth/built/node_support/node_requestor';
import {
  GRANT_TYPE_AUTHORIZATION_CODE,
  GRANT_TYPE_REFRESH_TOKEN,
  TokenRequest
} from '@openid/appauth/built/token_request';
import { BaseTokenRequestHandler } from '@openid/appauth/built/token_request_handler';

import { EventEmitter } from 'events';

export class AuthStateEmitter extends EventEmitter {
  static ON_TOKEN_RESPONSE = 'on_token_response';
}

/* the Node.js based HTTP client. */
const requestor = new NodeRequestor();

/* an example open id connect provider */
const openIdConnectUrl = 'https://dev-669532.oktapreview.com/oauth2/default';

/* example client configuration */
const clientId = '0oafavni96j2yJNvQ0h7';
const redirectUri = 'http://127.0.0.1:8000';
const scope = 'openid profile offline_access';

export default class AuthService {
  notifier;
  authorizationHandler;
  tokenHandler;
  authStateEmitter;
  challengePair;

  // state
  configuration;
  refreshToken;
  accessTokenResponse;

  constructor() {
    this.notifier = new AuthorizationNotifier();
    this.authStateEmitter = new AuthStateEmitter();
    this.authorizationHandler = new NodeBasedHandler();
    this.tokenHandler = new BaseTokenRequestHandler(requestor);
    // set notifier to deliver responses
    this.authorizationHandler.setAuthorizationNotifier(this.notifier);
    // set a listener to listen for authorization responses
    // make refresh and access token requests.
    this.notifier.setAuthorizationListener((request, response, error) => {
      console.log('Authorization request complete ', request, response, error);
      if (response) {
        this.makeRefreshTokenRequest(response.code)
            .then(result => this.performWithFreshTokens())
            .then(() => {
              this.authStateEmitter.emit(AuthStateEmitter.ON_TOKEN_RESPONSE);
              console.log('All Done.');
            })
      }
    });
  }

  fetchServiceConfiguration() {
    return AuthorizationServiceConfiguration
        .fetchFromIssuer(openIdConnectUrl, requestor)
        .then(response => {
          console.log('Fetched service configuration', response);
          this.configuration = response;
        });
  }

  makeAuthorizationRequest(username) {
    if (!this.configuration) {
      console.log('Unknown service configuration');
      return;
    }

    const extras = {'prompt': 'consent', 'access_type': 'offline'};
    if (username) {
      extras['login_hint'] = username;
    }

    this.challengePair = AuthService.getPKCEChallengePair();

    // PKCE
    extras['code_challenge'] = this.challengePair.challenge;
    extras['code_challenge_method'] = 'S256';

    // create a request
    const request = new AuthorizationRequest(
        clientId, redirectUri, scope, AuthorizationRequest.RESPONSE_TYPE_CODE,
        undefined /* state */, extras);

    console.log('Making authorization request ', this.configuration, request);

    this.authorizationHandler.performAuthorizationRequest(
        this.configuration, request);
  }

  makeRefreshTokenRequest(code) {
    if (!this.configuration) {
      console.log('Unknown service configuration');
      return Promise.resolve();
    }

    let tokenRequestExtras = { code_verifier: this.challengePair.verifier };

    // use the code to make the token request.
    let request = new TokenRequest(
        clientId, redirectUri, GRANT_TYPE_AUTHORIZATION_CODE, code, undefined, tokenRequestExtras);

    return this.tokenHandler.performTokenRequest(this.configuration, request)
        .then(response => {
          console.log(`Refresh Token is ${response.refreshToken}`);
          this.refreshToken = response.refreshToken;
          this.accessTokenResponse = response;
          return response;
        })
        .then(() => {});
  }

  loggedIn() {
    return !!this.accessTokenResponse && this.accessTokenResponse.isValid();
  }

  signOut() {
    // forget all cached token state
    this.accessTokenResponse = null;
  }

  performWithFreshTokens() {
    if (!this.configuration) {
      console.log('Unknown service configuration');
      return Promise.reject('Unknown service configuration');
    }
    if (!this.refreshToken) {
      console.log('Missing refreshToken.');
      return Promise.resolve('Missing refreshToken.');
    }
    if (this.accessTokenResponse && this.accessTokenResponse.isValid()) {
      // do nothing
      return Promise.resolve(this.accessTokenResponse.accessToken);
    }
    let request = new TokenRequest(
        clientId, redirectUri, GRANT_TYPE_REFRESH_TOKEN, undefined,
        this.refreshToken);
    return this.tokenHandler.performTokenRequest(this.configuration, request)
        .then(response => {
          this.accessTokenResponse = response;
          return response.accessToken;
        });
  }

  static getPKCEChallengePair() {
    let verifier = AuthService.base64URLEncode(crypto.randomBytes(32));
    let challenge = AuthService.base64URLEncode(AuthService.sha256(verifier));
    return {verifier, challenge};
  }

  static base64URLEncode(str) {
    return str.toString('base64')
      .replace(/\+/g, '-')
      .replace(/\//g, '_')
      .replace(/=/g, '');
  }

  static sha256(buffer) {
    return crypto.createHash('sha256').update(buffer).digest();
  }
}

[REQUIRED] Actual Behavior

Compile error.

[REQUIRED] Steps to reproduce the behavior

I created a GitHub repo to show this issue: https://github.com/mraible/appauth-react-electron.

Steps to reproduce:

  1. git clone https://github.com/mraible/appauth-react-electron.git
  2. npm i && npm run build
  3. See the error in your console
  4. Modify src/Login.js to remove all references to AuthService
  5. Run npm run build and everything will compile OK

[REQUIRED] Environment

mraible commented 6 years ago

It seems like I just need to exclude node_modules/opener/opener.js from the build process. I've tried running yarn eject on my app and configuring webpack to use shebang-loader for this file, but no luck. I still get the same error.

// Process JS with Babel.
{
  test: /\.(js)$/,
  include: paths.appNodeModules + '/opener/opener.js',
  loader: [require.resolve('shebang-loader'), require.resolve('babel-loader')],
},
{
  test: /\.(js|jsx|mjs)$/,
  include: paths.appSrc,
  exclude: paths.appNodeModules + '/opener/opener.js',
  loader: require.resolve('babel-loader'),
  options: {

    // This is a feature of `babel-loader` for webpack (not Babel itself).
    // It enables caching results in ./node_modules/.cache/babel-loader/
    // directory for faster rebuilds.
    cacheDirectory: true,
  },
},
tikurahul commented 6 years ago

I think the problem here might be that opener.js has a #! preamble. You might want to do a pre-processing step which just removes that line before you run through the react build process.

mraible commented 6 years ago

I tried removing #!/usr/bin/env node from ./node_modules/opener/opener.js and that got me a bit further, but not much.

Failed to minify the code from this file: 

        ./node_modules/hoek/lib/index.js:35 

Is this a dependency of AppAuth JS?

tikurahul commented 6 years ago

hoek/lib is not a dependency.

mraible commented 6 years ago

According to npm list hoek, it is a dependency of @openid/appauth:

➜  foo git:(master) ✗ npm list hoek
foo@0.1.0 /Users/mraible/foo
└─┬ @openid/appauth@0.3.4
  └─┬ hapi@17.5.1
    ├─┬ accept@3.0.2
    │ └── hoek@5.0.3  deduped
    ├─┬ ammo@3.0.1
    │ └── hoek@5.0.3  deduped
    ├─┬ boom@7.2.0
    │ └── hoek@5.0.3  deduped
    ├─┬ bounce@1.2.0
    │ └── hoek@5.0.3  deduped
    ├─┬ call@5.0.1
    │ └── hoek@5.0.3  deduped
    ├─┬ catbox@10.0.2
    │ └── hoek@5.0.3  deduped
    ├─┬ catbox-memory@3.1.2
    │ └── hoek@5.0.3  deduped
    ├─┬ heavy@6.1.0
    │ └── hoek@5.0.3  deduped
    ├── hoek@5.0.3
    ├─┬ joi@13.3.0
    │ └── hoek@5.0.3  deduped
    ├─┬ mimos@4.0.0
    │ └── hoek@5.0.3  deduped
    ├─┬ podium@3.1.2
    │ └── hoek@5.0.3  deduped
    ├─┬ shot@4.0.5
    │ └── hoek@5.0.3  deduped
    ├─┬ statehood@6.0.6
    │ ├── hoek@5.0.3  deduped
    │ └─┬ iron@5.0.4
    │   └── hoek@5.0.3  deduped
    ├─┬ subtext@6.0.7
    │ ├── hoek@5.0.3  deduped
    │ ├─┬ pez@4.0.2
    │ │ ├── hoek@5.0.3  deduped
    │ │ └─┬ nigel@3.0.1
    │ │   ├── hoek@5.0.3  deduped
    │ │   └─┬ vise@3.0.0
    │ │     └── hoek@5.0.3  deduped
    │ └─┬ wreck@14.0.2
    │   └── hoek@5.0.3  deduped
    └─┬ topo@3.0.0
      └── hoek@5.0.3  deduped
tikurahul commented 6 years ago

Ah.. So it's a dependency of hapi. Let me try and reproduce this on my end and investigate. I have been able to use rollup to build AppAuth-JS for prod mode. I used a configuration that looks something like:

import closure from 'rollup-plugin-closure-compiler-js';
import commonjs from 'rollup-plugin-commonjs';
import nodeResolve from 'rollup-plugin-node-resolve';
import replace from 'rollup-plugin-replace';

export default {
  input: 'built/src/index.js',
  output: {
    file: 'built/app-bundle.js',
    format: 'iife',
    name: 'app',
    globals: {'crypto': 'crypto'},
    sourcemap: true
  },
  external: ['crypto'],
  plugins: [
    replace({'process.env.NODE_ENV': JSON.stringify('production')}),
    nodeResolve({jsnext: true, main: true}),
    commonjs(),
    closure({createSourceMap: true}),
  ]
};

And use something like:

node_modules/.bin/tsc && node_modules/.bin/rollup --config config/rollup/prod.js

My devDependencies looks like:

"devDependencies": {
    "npm": "^6.0.0",
    "rollup": "^0.58.2",
    "rollup-plugin-closure-compiler-js": "^1.0.6",
    "rollup-plugin-commonjs": "^9.1.0",
    "rollup-plugin-node-resolve": "^3.3.0",
    "rollup-plugin-replace": "^2.0.0",
    "rollup-watch": "^4.3.1",
    "typescript": "^2.8.3"
  }
mraible commented 6 years ago

I was able to use electron-builder to package the sample for production and prove it works. The problem is centered around CRA. I'd be a very happy camper if I was able to make the AuthService class above compile in a CRA-generated app.

When I tried to port my app from CRA to be in the appauth-js-electron-sample, I had to change everything to use TypeScript (and JXS). The TypeScript stuff went OK, but trying to figure out Babel and JSX proved frustrating. Ideally, it'd be possible to modify my existing app to somehow work with AuthService (even if it requires ejecting the webpack config).

tikurahul commented 6 years ago

I find trying to keep up with all the changes in webpack frustrating. I use rollup which does a really good job especially combined with closure-compiler.

I will add documentation and sample code around how one can build AppAuth-JS for production (there is a browserify example at https://github.com/openid/AppAuth-JS/blob/master/package.json#L24) but I will also upload a sample that uses rollup.

I will also investigate on what I should be doing to make AppAuth-JS work better with CRA.

tikurahul commented 6 years ago

Closing this for now. Will investigate using Webpack, and update this thread.

zkewal commented 6 years ago
I am facing this error in an angular-electron application. ` ERROR in ./node_modules/opener/opener.js Module parse failed: Unexpected character '#' (1:0) You may need an appropriate loader to handle this file type. #!/usr/bin/env node
"use strict";

ℹ 「wdm」: Failed to compile. `

I started using the starter from this repository: https://github.com/maximegris/angular-electron

Included AppAuth-js as dependency and I am facing this error.

ng eject is disabled in current version of angular cli so I cannot modify webpack loader to remove #! as a prebuild step.

Is there any other step I should take to get rid of this error?

ildarnm commented 6 years ago

@zkewal I have same error. Did you find workaround?