Giter Club home page Giter Club logo

imperial's Introduction

Imperial

Imperial is a Federated Login service, allowing you to easily integrate your Vapor applications with OAuth providers to handle your apps authentication.

Attribution

Author(s): @calebkleveter, @rafiki270

License

All code contained in the Imperial package is under the MIT license agreement.

imperial's People

Contributors

0xtim avatar 123flo321 avatar calebkleveter avatar cweinberger avatar dannys42 avatar dylanshine avatar fananek avatar gaetandezeiraud avatar gwynne avatar hejki avatar inukvt avatar jeronimopaganini avatar jinthagerman avatar m-barthelemy avatar microtherion avatar natebird avatar rafiki270 avatar rasmusebbesen avatar robertomachorro avatar the0xalex avatar wibed avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

imperial's Issues

Can't set client ID and secret directly

Limiting user only to ENV is ... well, limiting. At the moment I can configure my sytem through either an ENV or a config json file (usually used locally for development as ENV get lost everytime you do vapor xcode) so now I can never start my app from xcode after I re-generate project.

I do have my own mechanisms to read ENV and having this separate just breaks the configuration unification of our product.

I am happy to implement whatever but would be awesome to hear some ideas about this first :)

Add logic for pulling and saving refresh tokens on sessions

It would be great to integrate the ability to pull and save refresh tokens on a session.

I was able to get this working locally for the Google oAuth service by adding access_type=offline&, at which point the refresh token gets returned from the Google API similar to the current flow for pulling and saving an access token.

This would be helpful for workflows where the user grants permission to the web app once, then the app periodically refreshes the access token with the refresh token behind the scenes and continues to interoperate with the linked account.

Cheers, and great library!

How to access JWT scopes?

I'm trying to use Imperial for Google Login with my app.

Is there any guide on how to obtain the mail address that is part of Google's JWT?

I can see that the token is sent as part of Google's payload, but is discarded as only the access token is stored in the session.

Is there a best practice on how to obtain the mail address?

Session access token encoding error for Facebook callback

I'm using Facebook provider for authentication in my Vapor 4 app. Everything works fine on localhost, but I get the following error for callback URL for the app deployed on Heroku: invalidValue("long_fb_code_here", Swift.EncodingError.Context(codingPath: [], debugDescription: "Top-level String encoded as string JSON fragment.", underlyingError: nil)). I've found out this happens when setting a session access token in FacebookRouter.swift:52. For some reason the encoder has troubles with encoding the received access token as a string in Sessions+Imperial.swift:62.

As I noted it works fine on localhost which is really strange and I have no clue why. 😅 I have everything set up fine on Facebook side. As a temporary workaround I forked this repo and replaced try session.setAccessToken(accessToken) with session.data["access_token"] = accessToken. That works fine. I really wonder why this is an issue only in the deployed app.

If it is relevant, I use fluent as storage for sessions.

Auth0 support missing

The documentation mentions being able to use Auth0, but there doesn't seem to be any support for it in the code. The docs mention a package called ImperialAuth-, which doesn't exist, and (assuming that that is a type and the '-' is actually meant to be a '0'), attempting to import ImperialAuth0 also fails.

Add tests

It would be great to add some tests to avoid regressions etc with builds 😃

Add More OAuth Providers

GitHub makes a good start for supported OAuth providers, but we need more. Below are a few I would like to add:

  • Twitter
  • Google
  • Facebook
  • Instagram

or any others that we think would be good.

Note to maintainer: Look into Auth0

Completion Closure Not Invoked When Using Google Auth

At the moment, I'm trying to use Imperial to allow users to sign in to me web app using Google Auth. I've set up my code as follows (see below). The issue I'm experiencing at the moment is that processGoogleLogin is never fired. If I navigate to /google then I do see the OAuth page and on logging in using my Google account I am redirected to /authenticated which is correct but I never received an access token back it would seem and completion closure never appears to fire. Is there a piece of the set up that I am missing perhaps?

Thanks in advance 🙏

routes.swift

let imperialController = ImperialController(sessionsMiddleware: app.sessions.middleware)
try appRoutes.register(collection: imperialController)

ImperialController.swift

class ImperialController {
    private let sessionsMiddleware: Middleware
    
    init(sessionsMiddleware: Middleware) {
        self.sessionsMiddleware = sessionsMiddleware
    }
    
    func processGoogleLogin(request: Request, token: String) throws -> EventLoopFuture<ResponseEncodable> {
        print(token)
        return request.eventLoop.future(request.redirect(to: "/"))
    }
}

extension ImperialController: RouteCollection {
    func boot(routes: RoutesBuilder) throws {
        let group = routes.grouped(sessionsMiddleware)
        try group.oAuth(
            from: Google.self,
            authenticate: "google",
            authenticateCallback: nil,
            callback: "http://localhost:8080/authenticated",
            scope: ["profile", "email"],
            completion: processGoogleLogin
        )
    }
}

Holding Strong Reference to Client

Somewhere we appear to be holding onto Client when the container is released, cause us to hit a fatalError in Vapor:

Fatal error: If you encounter this error, you are holding on to a client after de-initializing your Application. This is usually a bad idea.: file /tmp/build_769e8eccc58c3bc6a5daf7c3903d82c0/.build/checkouts/vapor.git-4085136337785951737/Sources/Vapor/Client/FoundationClient.swift, line 16

This is a design issue on our side.

can't find www.accounts.google.com

Found that Google login is not working due to inability to find domain name:

nslookup www.accounts.google.com
Server:		127.0.0.53
Address:	127.0.0.53#53

** server can't find www.accounts.google.com: NXDOMAIN

Checked it on my computer and two other servers (in Germany and USA), there is the same result.

PR #74

id like to rename callback to redirectURL

variable representing the redirectURL, which is dictated by the oauth2 provider.
is named callback.

example:

public class GitlabRouter: FederatedServiceRouter {
...
    public required init(callback: String, completion: @escaping (Request, String) throws ->
...

a few router have a callbackURL, representing the same thing this hardcoded. example:

    public static var baseURL: String = "https://gitlab.com/"
    public static var callbackURL: String = "callback"

on top a function which handles the "callback" made on the "callback" sometimes referred to as "callbackURL" is named... you guessed it. "callback"

    public func callback(_ request: Request) throws -> EventLoopFuture<Response> {
        return try self.fetchToken(from: request).flatMap { accessToken in
            let session = request.session
            do {
                try session.setAccessToken(accessToken)
                try session.set("access_token_service", to: self.service)
                return try self.callbackCompletion(request, accessToken).flatMap { response in
                    return response.encodeResponse(for: request)
                }
            } catch {
                return request.eventLoop.makeFailedFuture(error)
            }
        }
    }

additionally callback is a reserved word, in a sense it is a descriptive term well known in IT.

for all these reasons, id like to rename it. following changes would apply

  • callback passed to FederatedServiceRouter should be named redirectURL/redirectURI, because it is the copied value from the oauth provider named redirect_uri .
  • callback function which handles the route for the redirect_uri, would be named handleoauthRedirect
  • files representing related content, GoogleCallbackBody.swift´, would be renamed to GoogleOauthRedirectBody.swift`

furthermore what is the background behind those static references?

// ImperialDiscord/DiscordRouter.swift
...
public class DiscordRouter: FederatedServiceRouter {
    public static var baseURL: String = "https://discord.com/"
    public static var callbackURL: String = "callback"
...
        var components = URLComponents()
        components.scheme = "https"
        components.host = "discord.com"
        components.path = "/api/oauth2/authorize"
        components.queryItems = [
            clientIDItem,
            .init(name: "redirect_uri", value: DiscordRouter.callbackURL),
            .init(name: "response_type", value: "code"),
            scopeItem
        ]

the auth url could only be wrong for it would not be dictated by the provider.
this ill let be, for the moment...

all is open for discussion! please let me know what you thing, your input and thoughts.

Missing code key in url query

Triggered by this code

this is the code in Imperial/FederatedServiceRouter that throws:

` public func fetchToken(from request: Request) throws -> EventLoopFuture {
let code: String
if let queryCode: String = try request.query.get(at: codeKey) {
code = queryCode
} else if let error: String = try request.query.get(at: errorKey) {
throw Abort(.badRequest, reason: error)
} else {
throw Abort(.badRequest, reason: "Missing 'code' key in URL query")
}

    let body = callbackBody(with: code)
    let url = URI(string: accessTokenURL)
    
    return body.encodeResponse(for: request)
        .map { $0.body.buffer }
        .flatMap { buffer in
            return request.client.post(url, headers: self.callbackHeaders) { $0.body = buffer }
        }.flatMapThrowing { response in
            return try response.content.get(String.self, at: ["access_token"])
        }
}`

Known issue people had already before:

Post here how sb fixed it

Alright. I think I figured it out...
What's the chance the data is being serialized as JSON even though I set static var defaultContentType: HTTPMediaType = .urlEncodedForm in LinkedInCallbackBody? The chance ended up being pretty high, given that adding a client.headers = HTTPHeaders(dictionaryLiteral: ("Content-Type", "application/x-www-form-urlencoded")) to the beforeSend closure eliminated the issue I was having.
That, or Vapor doesn't automatically set that header... which would make more sense.

public func fetchToken(from request: Request) throws -> EventLoopFuture<String> {
    let code: String
    if let queryCode: String = try request.query.get(at: "code") {
        code = queryCode
    } else if let error: String = try request.query.get(at: "error") {
        throw Abort(.badRequest, reason: error)
    } else {
        throw Abort(.badRequest, reason: "Missing 'code' key in URL query")
    }
    
    let body = LinkedInCallbackBody(code: code, clientId: self.tokens.clientID, clientSecret: self.tokens.clientSecret, redirectURI: self.callbackURL)
    
    let url = URI(string: self.accessTokenURL)
    return body.encodeResponse(for: request).map {
        $0.body
    }.flatMap { body in
        return request.client.post(url, beforeSend: { client in
            client.body = body.buffer
            client.headers = HTTPHeaders(dictionaryLiteral: ("Content-Type", "application/x-www-form-urlencoded"))
        })
    }.flatMapThrowing { response in
        return try response.content.get(String.self, at: ["access_token"])
    }
}

<@!432065887202181142> This is what the (working) method ended up being. I'm willing to share my Imperial+LinkedIn code, if you think that'd be of any use.

I'm not quite good/confident enough with this to feel 100% comfortable submitting a PR, but maybe I should anyway...

public func fetchToken(from request: Request) throws -> EventLoopFuture<String> {
    let code: String
    if let queryCode: String = try request.query.get(at: "code") {
        code = queryCode
    } else if let error: String = try request.query.get(at: "error") {
        throw Abort(.badRequest, reason: error)
    } else {
        throw Abort(.badRequest, reason: "Missing 'code' key in URL query")
    }
    
    let body = LinkedInCallbackBody(code: code, clientId: self.tokens.clientID, clientSecret: self.tokens.clientSecret, redirectURI: self.callbackURL)
    
    let url = URI(string: self.accessTokenURL)
    return body.encodeResponse(for: request).map {
        $0.body
    }.flatMap { body in
        return request.client.post(url, beforeSend: { client in
            client.body = body.buffer
            client.headers = HTTPHeaders(dictionaryLiteral: ("Content-Type", "application/x-www-form-urlencoded"))
        })
    }.flatMapThrowing { response in
        return try response.content.get(String.self, at: ["access_token"])
    }
}

<@!432065887202181142> This is what the (working) method ended up being. I'm willing to share my Imperial+LinkedIn code, if you think that'd be of any use.

I'm not quite good/confident enough with this to feel 100% comfortable submitting a PR, but maybe I should anyway...

public func fetchToken(from request: Request) throws -> EventLoopFuture<String> {
    let code: String
    if let queryCode: String = try request.query.get(at: "code") {
        code = queryCode
    } else if let error: String = try request.query.get(at: "error") {
        throw Abort(.badRequest, reason: error)
    } else {
        throw Abort(.badRequest, reason: "Missing 'code' key in URL query")
    }
    
    let body = LinkedInCallbackBody(code: code, clientId: self.tokens.clientID, clientSecret: self.tokens.clientSecret, redirectURI: self.callbackURL)
    
    let url = URI(string: self.accessTokenURL)
    return body.encodeResponse(for: request).map {
        $0.body
    }.flatMap { body in
        return request.client.post(url, beforeSend: { client in
            client.body = body.buffer
            client.headers = HTTPHeaders(dictionaryLiteral: ("Content-Type", "application/x-www-form-urlencoded"))
        })
    }.flatMapThrowing { response in
        return try response.content.get(String.self, at: ["access_token"])
    }
}

<@!432065887202181142> This is what the (working) method ended up being. I'm willing to share my Imperial+LinkedIn code, if you think that'd be of any use.

I'm not quite good/confident enough with this to feel 100% comfortable submitting a PR, but maybe I should anyway...

I cannot fix this in the source code but as I saw other people are having the same issue, maybe this bug could be fixed.

Bug with coding strategy

When I use
`
let encoder = JSONEncoder()
encoder.keyEncodingStrategy = .convertToSnakeCase
encoder.dateEncodingStrategy = .iso8601
ContentConfiguration.global.use(encoder: encoder, for: .json)

let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
decoder.dateDecodingStrategy = .iso8601
ContentConfiguration.global.use(decoder: decoder, for: .json)
`

in configure.swift I haw error -
image

but when I don't use coding strategy I got access token from Google. And it work, for testing I use project in book "Server-Side Swift with Vapor" (edition 3) Chapter 22: Google Authentication.

Unit tests crash with "Fatal error: Unexpectedly found nil while unwrapping an Optional value"

Unit tests crash in XCTestCase subclasses inside of override func setUp() async throws at the point where .oAuth(from:authenticate:callback:scope:completion:) is called in production code.

The stack trace shows OAuthService.services.getter force unwrapping the global services variable, which is of type ThreadSpecificVariable<OAuthServiceContainer>.

According to the headerdocs for ThreadSpecificVariable.init(value:):

/// Initialize a new ThreadSpecificVariable with value for the calling thread. After calling this, the calling
/// thread will see currentValue == value but on all other threads currentValue will be nil until changed.

I suspect this is a multithreading issue. Maybe the first instance of a test class (to run testFoo) sets up the OAuth route on Thread A, and the next instance of the test class (to run testBar) tries to set up the OAuth route on Thread B.

I was, in fact, able to confirm this with breakpoints in Xcode. Anytime setUp() jumps to a different thread, the test run will crash.

Here is an example minimal project that reproduces the issue.

Custom ENV variables

It would be amazing if we could specify our own ENV variables so they can be defined in a uniform way in our application

Fix Typos and Bad Image URLs

Fix:

  • 'Note: Their' to 'Note: There'.
  • Images in Google doc are broken.
  • Callback URI for Google auth provider in router.oAuth.

How to use with Vapor 4?

Adding Imperial to the Package.swift is producing and error. The format of the Package.swift has changed.

error: unknown package 'imperial' in dependencies of target 'App'

// swift-tools-version:5.2
import PackageDescription

let package = Package(
    name: "PainFreeJits",
    platforms: [
       .macOS(.v10_15),
    ],
    products: [
        .executable(name: "Run", targets: ["Run"]),
        .library(name: "App", targets: ["App"]),
    ],
    dependencies: [
        // 💧 A server-side Swift web framework.
        .package(url: "https://github.com/vapor/vapor.git", from: "4.3.0"),
        .package(url: "https://github.com/vapor/leaf.git", from: "4.0.0-rc"),
        .package(url: "https://github.com/vapor/fluent.git", from: "4.0.0-rc.1"),
        .package(url: "https://github.com/vapor/fluent-sqlite-driver.git", from: "4.0.0-rc.1"),
        .package(url: "https://github.com/vapor-community/Imperial.git", from: "0.7.1"),
    ],
    targets: [
        .target(name: "App", dependencies: [
            .product(name: "Leaf", package: "leaf"),
            .product(name: "Fluent", package: "fluent"),
            .product(name: "FluentSQLiteDriver", package: "fluent-sqlite-driver"),
            .product(name: "Vapor", package: "vapor"),
            .product(name: "Imperial", package: "imperial"),
        ]),
        .target(name: "Run", dependencies: [
            .target(name: "App"),
        ]),
        .testTarget(name: "AppTests", dependencies: [
            .target(name: "App"),
            .product(name: "XCTVapor", package: "vapor"),
        ])
    ]
)

Is it a way not make Github call directly backend, but let Github call frontend first, and frontend call gh-auth-complete in backend and pass code?

I did setup Imperial as tutorial says, but when a GET is sent with http://localhost:8080/gh-auth-complete?code=1c529782861a44782488 then it refuses, it does not have access-token.

My frontend and backend is separated. I would use Oauth 2.0 in the official way, Github -> frontend -> backend.

Is it possible to pass only code and check its validity in a next communication sequence?

oauth

Value required for key 'access_token'.

Hey! Can you please tell me what is the problem?
I receive a response from FB and Google with the code but I cannot process it. The execution process is interrupted before reaching the last argument "completion" to process the incoming token and then get user data
try routes.oAuth(from: Facebook.self, authenticate: "login-facebook", callback: facebookCallbackURL, scope: ["public_profile", "email"], completion: processFacebookLogin)
Снимок экрана 2021-03-22 в 18 21 13

Shopify auth broken

Some recent changes changed the call ordering which ShopifyRouter was expecting. Specifically, the authURL is dynamically generated since it generates a unique nonce and stores it in a cookie. Also, the request url contains a param that is required to generate the authURL.

Current code:

   public func generateAuthenticationURL(request: Request) throws -> URL {
        let nonce = String(UUID().uuidString.prefix(6))
        try request.session().setNonce(nonce)
        return try authURLFrom(request, nonce: nonce)
    }

    private func authURLFrom(_ request: Request, nonce: String) throws -> URL {
        guard let shop = request.query[String.self, at: "shop"] else { throw Abort(.badRequest) }
        
        return URL(string: "https://\(shop)/admin/oauth/authorize?" + "client_id=\(tokens.clientID)&" +
            "scope=\(scope.joined(separator: ","))&" +
            "redirect_uri=\(callbackURL)&" +
            "state=\(nonce)")!
    }

This was expected to be called

    public func authenticate(_ request: Request) throws -> Future<Response> {
        
        _authURL = try generateAuthenticationURL(request: request).absoluteString
        let redirect: Response = request.redirect(to: _authURL)
        return request.eventLoop.newSucceededFuture(result: redirect)
    }

before

    public var authURL: String {
        return _authURL
    }

Would you suggest the protocol definition for authURL change to:
func authURL(req: Request) -> String?

Docker build error - ambiguous use of 'hex'

When running tests inside a Docker container I get the following error:

/package/.build/checkouts/Imperial/Sources/Imperial/Services/Shopify/URL+Shopify.swift:13:10: error: ambiguous use of 'hex'
swapper_1_c2f870a92ea5 |                 return hmac.hex
swapper_1_c2f870a92ea5 |                        ^
swapper_1_c2f870a92ea5 | /package/.build/checkouts/Imperial/Sources/Imperial/Services/Shopify/URL+Shopify.swift:26:16: note: found this candidate
swapper_1_c2f870a92ea5 |     public var hex: String {
swapper_1_c2f870a92ea5 |                ^
swapper_1_c2f870a92ea5 | /package/.build/checkouts/vapor/Sources/Vapor/Utilities/Bytes+Hex.swift:2:16: note: found this candidate
swapper_1_c2f870a92ea5 |     public var hex: String {
swapper_1_c2f870a92ea5 |                ^
swapper_1_c2f870a92ea5 | [2239/2281] Compiling Imperial GoogleJWTResponse.swift
swapper_1_c2f870a92ea5 | /package/.build/checkouts/Imperial/Sources/Imperial/Services/Shopify/URL+Shopify.swift:13:10: error: ambiguous use of 'hex'
swapper_1_c2f870a92ea5 |                 return hmac.hex
swapper_1_c2f870a92ea5 |                        ^
swapper_1_c2f870a92ea5 | /package/.build/checkouts/Imperial/Sources/Imperial/Services/Shopify/URL+Shopify.swift:26:16: note: found this candidate
swapper_1_c2f870a92ea5 |     public var hex: String {
swapper_1_c2f870a92ea5 |                ^
swapper_1_c2f870a92ea5 | /package/.build/checkouts/vapor/Sources/Vapor/Utilities/Bytes+Hex.swift:2:16: note: found this candidate
swapper_1_c2f870a92ea5 |     public var hex: String {
swapper_1_c2f870a92ea5 |                ^
swapper_1_c2f870a92ea5 | [2240/2281] Compiling Imperial GoogleJWTRouter.swift
swapper_1_c2f870a92ea5 | /package/.build/checkouts/Imperial/Sources/Imperial/Services/Shopify/URL+Shopify.swift:13:10: error: ambiguous use of 'hex'
swapper_1_c2f870a92ea5 |                 return hmac.hex

It seems like the package has a conflict with Vapor package extension. I don't have any custom similar extensions in the project. This only happens when running tests inside a Docker container. Everything works fine in XCode.

Imperial version: 1.0.0-beta.1
Vapor version: 4.14.0

Redirect Url not called

Hi, I have a problem with the login functionality on facebook, when I call facebook and give me access to the token, the callback is not called and I can't create the user.

How can i fix this?

Fix Model Storage in Sessions

The ability to store Codable types in a session has been removed from Vapor 3, so instead we need to encode the model the JSON data and convert it to a String so we can store it.

How can I get Google authURL?

Hi,
I have setup like indicated in the Google guide.
When I'm adding a route in an protected route (with ImperialMiddleware) , I get following message User currently not authenticated. But, I want to redirect the user to the Google Login Page, for authentification.

But, I can not access to authURL in GoogleRouter for set this value in ImperialMiddleware(redirect: <#T##String?#>). How can I do it?

Thanks.

We need working example for Imperial

Could you please add working example for Imperial library? I'm not feel good with Usage Guide, especially for Google sign in.
It will be very helpful for newbies, like me.

Vapor 4 update

Potentially starting a new project in the not too distant future that will need to use Shopify. Hoping to use Vapor and would be great to be able to use this package with Vapor 4.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.