text: import { encodeQuery, parseQuery, normalizePath } from '../utilities'
  import nanoid from 'nanoid'
  import * as jwt from "jsonwebtoken";
  
  const isHttps = process.server ? require('is-https') : null
  
  const DEFAULTS = {
    token_type: 'Bearer',
    response_type: 'token',
    tokenName: 'Authorization',
    token_key: 'access_token',
    refresh_token_key: 'refresh_token'
  }
  
  export default class Oauth2Scheme {
    constructor (auth, options) {
      this.$auth = auth
      this.req = auth.ctx.req
      this.name = options._name
  
      this.options = Object.assign({}, DEFAULTS, options)
    }
  
    get _scope () {
      return Array.isArray(this.options.scope)
        ? this.options.scope.join(' ')
        : this.options.scope
    }
  
    get _redirectURI () {
      const url = this.options.redirect_uri
  
      if (url) {
        return url
      }
  
      if (process.server && this.req) {
        const protocol = 'http' + (isHttps(this.req) ? 's' : '') + '://'
  
        return protocol + this.req.headers.host + this.$auth.options.redirect.callback
      }
  
      if (process.client) {
        return window.location.origin + this.$auth.options.redirect.callback
      }
    }
  
    async mounted () {
      // Sync token
      const token = this.$auth.syncToken(this.name)
      // Set axios token
      if (token) {
        this._setToken(token)
      }
  
      // Handle callbacks on page load
      const redirected = await this._handleCallback()
  
      if (!redirected) {
        return this.$auth.fetchUserOnce()
      }
    }
  
    _setToken (token) {
      // Set Authorization token for all axios requests
      this.$auth.ctx.app.$axios.setHeader(this.options.tokenName, token)
    }
  
    _clearToken () {
      // Clear Authorization token for all axios requests
      this.$auth.ctx.app.$axios.setHeader(this.options.tokenName, false)
    }
  
    async reset () {
      this._clearToken()
  
      this.$auth.setUser(false)
      this.$auth.setToken(this.name, false)
      this.$auth.setRefreshToken(this.name, false)
  
      return Promise.resolve()
    }
  
    async login ({ params, state, nonce } = {}) {
      const opts = {
        protocol: 'oauth2',
        response_type: this.options.response_type,
        access_type: this.options.access_type,
        client_id: this.options.client_id,
        redirect_uri: this._redirectURI,
        scope: this._scope,
        // Note: The primary reason for using the state parameter is to mitigate CSRF attacks.
        // https://auth0.com/docs/protocols/oauth2/oauth-state
        state: state || nanoid(),
        code_challenge_method: 'S256',
        ...params
      }
  
      if (this.options.audience) {
        opts.audience = this.options.audience
      }
  
      // Set Nonce Value if response_type contains id_token to mitigate Replay Attacks
      // More Info: https://openid.net/specs/openid-connect-core-1_0.html#NonceNotes
      // More Info: https://tools.ietf.org/html/draft-ietf-oauth-v2-threatmodel-06#section-4.6.2
      if (opts.response_type.includes('id_token')) {
        // nanoid auto-generates an URL Friendly, unique Cryptographic string
        // Recommended by Auth0 on https://auth0.com/docs/api-auth/tutorials/nonce
        opts.nonce = nonce || nanoid()
      }
      if (opts.code_challenge_method) {
        switch (opts.code_challenge_method) {
          case 'plain':
          case 'S256':
            {
              const state = nanoid()
              localStorage.setItem('pkce_state', state)
              const codeVerifier = nanoid(64)
              localStorage.setItem('pkce_code_verifier', codeVerifier)
              const codeChallenge = await this.pkceChallengeFromVerifier(codeVerifier, opts.code_challenge_method === 'S256')
              opts.code_challenge = window.encodeURIComponent(codeChallenge)
            }
            break
          case 'implicit':
          default:
            break
        }
      }
      if (this.options.response_mode) {
        opts.response_mode = this.options.response_mode
      }
      if (this.options.acr_values) {
        opts.acr_values = this.options.acr_values
      }
  
      this.$auth.$storage.setUniversal(this.name + '.state', opts.state)
  
      const url = this.options.authorization_endpoint + '?' + encodeQuery(opts)
  
      window.location = url
    }
  
    sha256 (plain) {
      const encoder = new TextEncoder()
      const data = encoder.encode(plain)
      return window.crypto.subtle.digest('SHA-256', data)
    }
  
    base64urlencode (str) {
      // Convert the ArrayBuffer to string using Uint8 array to convert to what btoa accepts.
      // btoa accepts chars only within ascii 0-255 and base64 encodes them.
      // Then convert the base64 encoded to base64url encoded
      //   (replace + with -, replace / with _, trim trailing =)
      return btoa(String.fromCharCode.apply(null, new Uint8Array(str)))
        .replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '')
    }
  
    async getRoles() {
      let token = await this.$auth.getToken(this.name)
      token = token.substr(token.indexOf(' ')+1)
      token = jwt.decode(token);
  
      return token.realm_access.roles
    }
  
    async pkceChallengeFromVerifier (v, hashValue) {
      if (hashValue) {
        const hashed = await this.sha256(v)
        return this.base64urlencode(hashed)
      }
      return v // plain is plain - url-encoded by default
    }
  
    async fetchUser () {
      if (!this.$auth.getToken(this.name)) {
        return
      }
  
      if (!this.options.userinfo_endpoint) {
        this.$auth.setUser({})
        return
      }
  
      const user = await this.$auth.requestWith(this.name, {
        url: this.options.userinfo_endpoint
      })
  
      const roles = await this.getRoles()
  
      this.$auth.setUser(user, roles)
    }
  
  
    async _handleCallback (uri) {
      // Handle callback only for specified route
      if (this.$auth.options.redirect && normalizePath(this.$auth.ctx.route.path) !== normalizePath(this.$auth.options.redirect.callback)) {
        return
      }
      // Callback flow is not supported in server side
      if (process.server) {
        return
      }
  
      const hash = parseQuery(this.$auth.ctx.route.hash.substr(1))
      const parsedQuery = Object.assign({}, this.$auth.ctx.route.query, hash)
      // accessToken/idToken
      let token = parsedQuery[this.options.token_key]
      // refresh token
      let refreshToken = parsedQuery[this.options.refresh_token_key]
  
      // Validate state
      const state = this.$auth.$storage.getUniversal(this.name + '.state')
      this.$auth.$storage.setUniversal(this.name + '.state', null)
      if (state && parsedQuery.state !== state) {
        return
      }
  
      // -- Authorization Code Grant --
      if (this.options.response_type === 'code' && parsedQuery.code) {
        const data = await this.$auth.request({
          method: 'post',
          url: this.options.access_token_endpoint,
          baseURL: process.server ? undefined : false,
          data: encodeQuery({
            code: parsedQuery.code,
            client_id: this.options.client_id,
            redirect_uri: this._redirectURI,
            response_type: this.options.response_type,
            audience: this.options.audience,
            grant_type: this.options.grant_type,
            code_verifier: (localStorage.getItem('pkce_code_verifier'))
          })
        })
  
        if (data[this.options.token_key]) {
          token = data[this.options.token_key]
        }
  
        if (data[this.options.refresh_token_key]) {
          refreshToken = data[this.options.refresh_token_key]
        }
      }
  
      if (!token || !token.length) {
        return
      }
  
      // Append token_type
      if (this.options.token_type) {
        token = this.options.token_type + ' ' + token
      }
  
      // Store token
      this.$auth.setToken(this.name, token)
  
      // Set axios token
      this._setToken(token)
  
      // Store refresh token
      if (refreshToken && refreshToken.length) {
        refreshToken = this.options.token_type + ' ' + refreshToken
        this.$auth.setRefreshToken(this.name, refreshToken)
      }
  
      // Redirect to home
      this.$auth.redirect('home', true)
  
      return true // True means a redirect happened
    }
  }
  