feathersjs-ecosystem / feathers-vuex

Integration of FeathersJS, Vue, and Nuxt for the artisan developer
https://vuex.feathersjs.com
MIT License
445 stars 110 forks source link

OAuth with Google and feathers-vuex #33

Closed jhlabs closed 7 years ago

jhlabs commented 7 years ago

Thanks for this fantastic plugin!

I cannot seem to properly construct the OAuth login flow with Google and hope the explanation to this issue could be helpful for many.

Steps to reproduce

I have connected the feathers-vuex plugin with the client as in the docs here:

'use strict'

import 'babel-polyfill'
import feathers from 'feathers/client'
import hooks from 'feathers-hooks'
import auth from 'feathers-authentication-client'
import restClient from 'feathers-rest/client'
import feathersVuex from 'feathers-vuex'
import axios from 'axios'
import store from '@/store/'

const feathersClient = feathers()
  .configure(hooks())
  .configure(restClient('http://localhost:3030').axios(axios))
  .configure(auth({ storage: window.localStorage }))
  .configure(feathersVuex(store, {
    idField: 'id',
    auth: {
      userService: '/users',
    }
  }))

feathersClient.service('/users')

export default feathersClient

On the server side I have created local and oauth authentication with the feathers generator. I added my google client secret and ID in the config.

Now I have created a sign up page:

<template>
  <v-card raised class="center-text white">
    <v-btn @click.native="googleSignUp" secondary raised dark>Sign up with Google</v-btn>
    <p class="my-3 center-text">or</p>
    <v-card-text>
       <v-text-field 
        class="center-input" 
        label="Your work email"
        type="text"
        prepend-icon="email"
        required
        v-model="email"
        name="email"
      ></v-text-field> 
      <v-text-field 
        class="center-input"
        label="Enter a secure password"
        required
        prepend-icon="fingerprint"
        v-model="password"
        :append-icon="passwordVisible ? 'visibility' : 'visibility_off'"
        :append-icon-cb="() => (passwordVisible = !passwordVisible)"
        :type="passwordVisible ? 'password' : 'text'"
      ></v-text-field>
    </v-card-text>
    <v-card-actions>
      <v-btn class="center-btn" primary raised dark @click.native="register(email,password)">Get started now</v-btn>
    </v-card-actions>
    <v-card-text>
      <p class="my-3 center-text">Already have an account?
        <router-link :to="{ name: 'login' }">Login here.</router-link>
      </p>
    </v-card-text>
  </v-card>
</template>

<script>
import { mapActions } from 'vuex'
export default {
  data () {
    return {
      company: undefined,
      email: undefined,
      password: undefined,
      passwordVisible: true
    }
  },
  methods: {
    register (email, password) {
      this.createUser({ email, password })
        .then(res => this.authenticate({strategy: 'local', email, password}))
        .then(this.$router.push('/'))
        .then(err => {
          console.log(err)
        })
    },
    googleSignUp () {
      window.location.href = '/auth/google'
    },
    ...mapActions('users', {
      createUser: 'create'
    }),
    ...mapActions('auth', [
      'authenticate'
    ])
  }
}
</script>

The local registration works flawlessly. For the oauth registration all that should be required on the client side is to link to the relative path '/auth/google' that has been created by feathers:

image

Expected behavior

The user should be directed to the Google OAuth page and then redirected to the pre-specified callback URL.

Actual behavior

If I use a relative path, the browser directs to a blank page with the given URL. When I change the URL to direct to the same URL on the server (by changing the port), the user is redirected to the Google OAuth page. However, the redirect does not seem to work back to the client.

I wonder which URL is the correct one (client or server side), and how the flow can be implemented correctly. Thanks a lot for your help!

System configuration

{
  "name": "seads-app",
  "version": "1.0.0",
  "description": "The client for the seads app.",
  "author": "Johannes Herrmann <johannes.herrmann2012@gmail.com>",
  "private": true,
  "scripts": {
    "dev": "node build/dev-server.js",
    "start": "node build/dev-server.js",
    "build": "node build/build.js",
    "unit": "cross-env BABEL_ENV=test karma start test/unit/karma.conf.js --single-run",
    "test": "npm run unit",
    "lint": "eslint --ext .js,.vue src test/unit/specs"
  },
  "dependencies": {
    "axios": "^0.16.2",
    "babel-polyfill": "^6.23.0",
    "feathers": "^2.1.4",
    "feathers-authentication-client": "^0.3.2",
    "feathers-hooks": "^2.0.1",
    "feathers-rest": "^1.8.0",
    "feathers-vuex": "^0.7.0",
    "vee-validate": "^2.0.0-rc.8",
    "vue": "^2.3.4",
    "vue-router": "^2.3.1",
    "vuetify": "^0.13.0",
    "vuex": "^2.3.1"
  },
  "devDependencies": {
    "autoprefixer": "^6.7.2",
    "babel-core": "^6.22.1",
    "babel-eslint": "^7.1.1",
    "babel-loader": "^6.2.10",
    "babel-plugin-istanbul": "^4.1.1",
    "babel-plugin-transform-object-rest-spread": "^6.23.0",
    "babel-plugin-transform-runtime": "^6.22.0",
    "babel-preset-env": "^1.3.2",
    "babel-preset-es2017": "^6.24.1",
    "babel-preset-stage-2": "^6.22.0",
    "babel-register": "^6.22.0",
    "chai": "^3.5.0",
    "chalk": "^1.1.3",
    "connect-history-api-fallback": "^1.3.0",
    "copy-webpack-plugin": "^4.0.1",
    "cross-env": "^4.0.0",
    "css-loader": "^0.28.0",
    "eslint": "^3.19.0",
    "eslint-config-airbnb-base": "^11.1.3",
    "eslint-config-standard": "^10.2.1",
    "eslint-friendly-formatter": "^2.0.7",
    "eslint-import-resolver-webpack": "^0.8.1",
    "eslint-loader": "^1.7.1",
    "eslint-plugin-html": "^2.0.0",
    "eslint-plugin-import": "^2.7.0",
    "eslint-plugin-node": "^5.1.1",
    "eslint-plugin-promise": "^3.5.0",
    "eslint-plugin-standard": "^3.0.1",
    "eventsource-polyfill": "^0.9.6",
    "express": "^4.14.1",
    "extract-text-webpack-plugin": "^2.0.0",
    "file-loader": "^0.11.1",
    "friendly-errors-webpack-plugin": "^1.1.3",
    "html-webpack-plugin": "^2.28.0",
    "http-proxy-middleware": "^0.17.3",
    "inject-loader": "^3.0.0",
    "karma": "^1.4.1",
    "karma-coverage": "^1.1.1",
    "karma-mocha": "^1.3.0",
    "karma-phantomjs-launcher": "^1.0.2",
    "karma-phantomjs-shim": "^1.4.0",
    "karma-sinon-chai": "^1.3.1",
    "karma-sourcemap-loader": "^0.3.7",
    "karma-spec-reporter": "0.0.30",
    "karma-webpack": "^2.0.2",
    "lolex": "^1.5.2",
    "mocha": "^3.2.0",
    "opn": "^4.0.2",
    "optimize-css-assets-webpack-plugin": "^1.3.0",
    "ora": "^1.2.0",
    "phantomjs-prebuilt": "^2.1.14",
    "rimraf": "^2.6.0",
    "semver": "^5.3.0",
    "shelljs": "^0.7.6",
    "sinon": "^2.1.0",
    "sinon-chai": "^2.8.0",
    "stylus": "^0.54.5",
    "stylus-loader": "^3.0.1",
    "url-loader": "^0.5.8",
    "vue-loader": "^11.3.4",
    "vue-style-loader": "^2.0.5",
    "vue-template-compiler": "^2.3.4",
    "webpack": "^2.3.3",
    "webpack-bundle-analyzer": "^2.2.1",
    "webpack-dev-middleware": "^1.10.0",
    "webpack-hot-middleware": "^2.18.0",
    "webpack-merge": "^4.1.0"
  },
  "engines": {
    "node": ">= 4.0.0",
    "npm": ">= 3.0.0"
  },
  "browserslist": [
    "> 1%",
    "last 2 versions",
    "not ie <= 8"
  ]
}
dortamiguel commented 7 years ago

In your backend you have to add on the config, in the "authentication": {} object, your oauth settings.

Also don't forget to add the redirect url on success login on the provider (Google in this case)

I also think that this problem doesn't belong to this repo, better try this one https://github.com/feathersjs/feathers-authentication

dortamiguel commented 7 years ago

Also, this code helps me a lot using authentication with feathers-vuex, maybe you can use it.

<template lang='pug'>

#app
    navbar(v-show='show_nav')

    #app_content
        router-view.view(
            v-if='check_meta()'
            v-bind='{ current_user }'
        )

</template>

<script>
import './app.sass'
import Navbar from './ui/navbar/navbar.vue'
import { mapActions, mapState, mapGetters } from 'vuex'

export default {
    name: 'app',

    components: { Navbar },

    data() {
        return { show_nav: true }
    },

    computed: {
        ...mapState('auth', ['user']),
        ...mapGetters('users', { getUserInStore: 'get' }),

        current_user() {
            if (this.user) return this.getUserInStore(this.user._id)
        },
    },

    methods: {
        ...mapActions('users', { getUser: 'get', patchUser: 'patch' }),

        fetch() {
            this.getUser(this.user._id)
        },

        check_meta() {
            const { auth, hide_nav } = this.$route.meta

            // Hides the navbar if necessary
            if (hide_nav) this.show_nav = false
            else this.show_nav = true

            // Wait until user is ready for auth required routes
            if (auth === true) {
                if (this.current_user) return true
                else return false
            } else return true
        },
    },

    watch: {
        user() {
            if (this.user) this.fetch()
        },
    },

    async created() {
        try {
            const not_oauth_browser = !!navigator.userAgent.indexOf('cordova')

            if (not_oauth_browser) {
                await this.$store.dispatch('auth/authenticate')

                // Redirect from home to profile select
                // if (this.$route.path === '/')
                //  this.$router.replace({ name: 'User' })
            }
        } catch (error) {
            if (
                error.message.includes('Could not find stored JWT') ||
                error.message.includes('_id is missing from current user.')
            ) {
                const token = localStorage.getItem('feathers-jwt')

                if (token) {
                    localStorage.removeItem('feathers-jwt')
                    document.cookie =
                        'feathers-jwt=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/'
                    location.reload()
                }
            } else {
                console.error(error)
            }
        }
    },
}
</script>
jhlabs commented 7 years ago

@ellipticaldoor thank you! I eventually got it to work with the help of this issue https://github.com/feathersjs/feathers-authentication/issues/493. The key for me was to use the server to call the '/auth/google' link and use the handler provided by @marshallswain. I also had to activate the Google + API in their console, which caused an error before.

Instead of using the parameter extraction that is recommended in the linked post, you can simply use this.$route.query.token to handle the final token with vue-router.

jappyjan commented 4 years ago

hello,

i'm having kinda the same problem (using github, but i don't think this makes a difference)...

how do i implement github/oauth flow? gui (vue) redirects to localhost:3030/oauth/github github is set to redirect on success to gui/oauth/github/callback

and then?