skelpo / UserManager

A small, useful user manager made for production application setups.
https://www.skelpo.com
MIT License
82 stars 5 forks source link

Suggestion: Add third-party oauth methods #14

Open dev4jam opened 6 years ago

dev4jam commented 6 years ago

Add third-party OAuth methods: FB, Twitter, Google, etc

proggeramlug commented 6 years ago

I love the idea, we need to do some research to figure out how to best solve this in a way that is not adding much overhead while also solving this generically.

The user manager is used for websites as well as apps as well as other services and so the solution needs to work for all of them.

Initially I would say that it should be like an "extra package" that can be installed as well but does not have to be. Thoughts on that? (this is open discussion)

calebkleveter commented 6 years ago

In my spare time I support a Vapor Community package called Imperial. This package handles that kind of auth, but aims for towards user end auth, meaning it redirects the user from your app to the OAuth provider's authentication page and then back again.

UserManager is an API for an app. There is no user interface (though one could be added). For the most part, this functionality should be handled by the app that connects to the UserManager's API.

dev4jam commented 6 years ago

@proggeramlug it's hard to say... basically it's an extension of AuthController (or just another FacebookAuthController, GoogleAuthController, etc). But this should be still a part of Auth service.

dev4jam commented 6 years ago

@calebkleveter, as I see Imperial does all the magic regarding the social oauth flow. it's not the part of API based service though because client sends ready-to-use social auth token to the server to process registration or sign-in.

dev4jam commented 6 years ago

Trying to implement FB sign-in/sign-up... What do you think:

struct FacebookData: Codable {
    var name : String
    var email : String
}

final class FacebookAuthController: RouteCollection {
    func boot(router: Router) throws {
        router.post("fbSignin", use: signin)
    }

    func signin(_ request: Request) throws -> Future<LoginResponse> {
        let signer = try request.make(JWTService.self)
        let client = try request.make(Client.self)
        let fbToken = try request.content.syncGet(String.self, at: "token")
        let facebookURL = "https://graph.facebook.com/me?fields=email,name,id,gender&access_token=\(fbToken)"

        let facebookResponse = client.get(facebookURL)
            .flatMap(to: FacebookData.self) { try $0.content.decode(FacebookData.self) }

        return facebookResponse.flatMap(to: (User?, FacebookData).self) { fbUser in
            return try User.query(on: request).filter(\.email == fbUser.email).first().and(result: fbUser)
        }
        .flatMap(to: User.self) { userInfo in
            if let user = userInfo.0 {
                return request.eventLoop.newSucceededFuture(result: user)
            } else {
                let user = try User(userInfo.1.email, "en")

                user.firstname = userInfo.1.name
                user.confirmed = true
                user.password = try BCrypt.hash(fbToken)

                return request.eventLoop.newSucceededFuture(result: user)
            }
        }
        .flatMap(to: User.self) { user in
            return user.save(on: request)
        }
        .flatMap(to: LoginResponse.self) { user in
            let userPayload = try Payload(user: user)

            let remotePayload = try request.payloadData(
                signer.sign(userPayload),
                with: ["userId": "\(user.requireID())"],
                as: JSON.self
            )

            // Create a response form the access token, refresh token. and user response data.
            return remotePayload.map(to: LoginResponse.self) { remotePayload in
                let payload = try remotePayload.merge(userPayload.json())

                let accessToken = try signer.sign(payload)
                let refreshToken = try signer.sign(RefreshToken(user: user))

                guard user.confirmed else { throw Abort(.badRequest, reason: "User not activated.") }

                let userResponse = UserResponse(user: user, attributes: nil)

                return LoginResponse(accessToken: accessToken, refreshToken: refreshToken, user: userResponse)
            }
        }
    }
}
proggeramlug commented 6 years ago

I'll need to play with it some more but I think this looks pretty good already!

I think we should offer this as some SPM package that can be included into the user-manager but doesn't necessarily have to. So people could configure their own flavor by just removing/adding the social media extensions they need.

What do you think? @calebkleveter @dev4jam

calebkleveter commented 6 years ago

I like it. Just note I don't have platforms such as Facebook, so I wouldn't be able to implement those providers.

proggeramlug commented 6 years ago

@dev4jam If you feel comfortable with that feel free to create such a package for Facebook since you have the code already - contact me or Caleb for any support you may need.

I'm thinking we should have packages such as: UserManager-Facebook UserManager-Twitter UserManager-LinkedIn UserManager-Google etc.

calebkleveter commented 6 years ago

@proggeramlug Would this be server-to-server authentication?

proggeramlug commented 6 years ago

In parts, depending on the platform it all works generically speaking like this:

  1. Request some token from (facebook/twitter/etc)
  2. Lead the user to a confirmation page that will authenticate this request
  3. receive your token and use it to verify the user

In some cases they actually do use JWTs for this or some variation of JWT. So there is some server-to-server but in all cases the user has to "manually" confirm it as well.

dev4jam commented 6 years ago

This is actually my first server-side code on Vapor... So I might not know some specifics... For example, I don't really understand how to register (in the router) this controller if it will be located in a separate package. But I can try :)

proggeramlug commented 6 years ago

My respect for trying and being involved, that is awesome!

If you just go ahead and put the controllers in a rep, I'm fairly sure we can figure out how to register them well. It probably needs to go through some middleware/service in the config.