Git Product home page Git Product logo

vapor-oauth's Introduction

   Vapor OAuth

Language Test Status Code Coverage MIT License

Vapor OAuth is an OAuth2 Provider Library written for Vapor. You can integrate the library into your server to provide authorization for applications to connect to your APIs.

It follows both RFC 6749 and RFC6750 and there is an extensive test suite to make sure it adheres to the specification.

It also implements the RFC 7662 specification for Token Introspection, which is useful for microservices with a shared, central authorization server.

Vapor OAuth supports the standard grant types:

  • Authorization Code
  • Client Credentials
  • Implicit Grant
  • Password Credentials

For an excellent description on how the standard OAuth flows work, and what to expect when using and implementing them, have a look at https://www.oauth.com.

Usage

Getting Started

Vapor OAuth can be added to your Vapor add with a simple provider. To get started, first add the library to your Package.swift dependencies:

dependencies: [
    ...,
    .package(url: "https://github.com/brokenhandsio/vapor-oauth", from: "0.6.0"))
]

Next import the library into where you set up your Droplet:

import VaporOAuth

Then add the provider to your Config:

try addProvider(VaporOAuth.Provider(codeManager: MyCodeManager(), tokenManager: MyTokenManager(), clientRetriever: MyClientRetriever(), authorizeHandler: MyAuthHandler(), userManager: MyUserManager(), validScopes: ["view_profile", "edit_profile"], resourceServerRetriever: MyResourceServerRetriever()))

To integrate the library, you need to set up a number of things, which implement the various protocols required:

  • CodeManager - this is responsible for generating and managing OAuth Codes. It is only required for the Authorization Code flow, so if you do not want to support this grant, you can leave out this parameter and use the default implementation
  • TokenManager - this is responsible for generating and managing Access and Refresh Tokens. You can either store these in memory, in Fluent, or with any backend.
  • ClientRetriever - this is responsible for getting all of the clients you want to support in your app. If you want to be able to dynamically add clients then you will need to make sure you can do that with your implementation. If you only want to support a set group of clients, you can use the StaticClientRetriever which is provided for you
  • AuthorizeHandler - this is responsible for allowing users to allow/deny authorization requests. See below for more details. If you do not want to support this grant type you can exclude this parameter and use the default implementation
  • UserManager - this is responsible for authenticating and getting users for the Password Credentials flow. If you do not want to support this flow, you can exclude this parameter and use the default implementation.
  • validScopes - this is an optional array of scopes that you wish to support in your system.
  • ResourceServerRetriever - this is only required if using the Token Introspection Endpoint and is what is used to authenticate resource servers trying to access the endpoint

Note that there are a number of default implementations for the different required protocols for Fluent in the Vapor OAuth Fluent package.

The Provider will then register endpoints for authorization and tokens at /oauth/authorize and /oauth/token

Protecting Endpoints

Vapor OAuth has a helper extension on Request to allow you to easily protect your API routes. For instance, let's say that you want to ensure that one route is accessed only with tokens with the profile scope, you can do:

try request.oauth.assertScopes(["profile"])

This will throw a 401 error if the token is not valid or does not contain the profile scope. This is so common, that there is a dedicated OAuth2ScopeMiddleware for this behaviour. You just need to initialise this with an array of scopes that must be required for that protect group. If you initialise it with a nil array, then it will just make sure that the token is valid.

You can also get the user with try request.oauth.user().

Protecting Resource Servers With Remote Auth Server

If you have resource servers that are not the same server as the OAuth server that you wish to protect using the Token Introspection Endpoint, things are slightly different. See the Token Introspection section for more information.

Grant Types

Authorization Code Grant

The Authorization Code flow is the most common flow used with OAuth. It is what most web applications will use for authorization with an OAuth Resource Server. The basic outline of this grant type is:

  1. A client (another app) redirects a resource owner (a user that holds information with you) to your Vapor app.
  2. Your Vapor app then authenticates the user and asks the user whether they want to allow the client access to the scopes requested (think logging into something with your Facebook account - it's this method).
  3. If the user approves the application then the OAuth server redirects back to the client with an OAuth Code (that is typically valid for 60s or so)
  4. The client can then exchange that code for an access and refresh token
  5. The client can use the access token to make requests to the Resource Server (the OAuth server, or your web app)

Implementation Details

As well as implementing the Code Manager, Token Manager, and Client Retriever, the most important part to implement is the AuthorizeHandler. Your authorize handler is responsible for letting the user decide whether they should let an application have access to their account. It should be clear and easy to understand what is going on and should be clear what the application is requesting access to.

It is your responsibility to ensure that the user is logged in and handling the case when they are not. An example implementation for the authorize handler may look something like:

func handleAuthorizationRequest(_ request: Request, authorizationGetRequestObject: AuthorizationGetRequestObject) throws -> ResponseRepresentable {
    guard request.auth.isAuthenticated(FluentOAuthUser.self) else {
        let redirectCookie = Cookie(name: "OAuthRedirect", value: request.uri.description)
        let response = Response(redirect: "/login")
        response.cookies.insert(redirectCookie)
        return response
    }

    var parameters = Node([:], in: nil)
    let client = clientRetriever.getClient(clientID: authorizationGetRequestObject.clientID)

    try parameters.set("csrf_token", authorizationGetRequestObject.csrfToken)
    try parameters.set("scopes", authorizationGetRequestObject.scopes)
    try parameters.set("client_name", client.clientName)
    try parameters.set("client_image", client.clientImage)
    try parameters.set("user", request.auth.user)

    return try view.make("authorizeApplication", parameters)
}

You need to add the SessionsMiddleware to your application for this flow to complete in order for the CSRF protection to work.

When submitting the authorize form back to Vapor OAuth, in the form data it must include:

  • applicationAuthorized - a boolean value to signify if the user allowed access to the client or not
  • csrfToken - the CSRF token supplied in the handler to protect against CSRF attacks

Implicit Grant

The Implicit Grant is almost identical to the Authorize Code flow, except instead of being redirected back with a code which you then exchange for a token, you get redirected back with the token in the fragment. It is up to the client (such as an iOS application) to then parse the token out of the redirect URI fragment.

This flow was designed for clients where you couldn't guarantee the security of the client secret, client-side apps, but has fallen out of favour recently and it is generally recommended to use the Authorization Code flow without a client secret instead.

Resource Owner Password Credentials Grant

The Password Credentials flow should only be used for first party applications, and Vapor OAuth mandates this. This flow allows the client to collect the username and password of the user and submit them directly to the OAuth server to get a token.

Note that if you are using the password flow, as per the specification, you must secure your endpoint against brute force attacks with rate limiting or generating alerts. The library will output a warning message to the console for any unauthorized attempts, which you can use for this purpose. The message is in the form of LOGIN WARNING: Invalid login attempt for user <USERNAME>.

Client Credentials Grant

Client Credentials is a userless flow and is designed for servers accessing other servers without the need for a user. Access is granted based upon the authentication of the client requesting access.

Token Introspection

If running a microservices architecture it is useful to have a single server that handles authorization, which all the other resource servers query. To do this, you can use the Token Introspection Endpoint extension. In Vapor OAuth, this adds an endpoint you can post tokens tokens at /oauth/token_info.

You can send a POST request to this endpoint with a single parameter, token, which contains the OAuth token you want to check. If it is valid and active, then it will return a JSON payload, that looks similar to:

{
    "active": true,
    "client_id": "ABDED0123456",
    "scope": "email profile",
    "exp": 1503445858,
    "user_id": "12345678",
    "username": "hansolo",
    "email_address": "[email protected]"
}

If the token has expired or does not exist then it will simply return:

{
    "active": false
}

This endpoint is protected using HTTP Basic Authentication so you need to send an Authorization: Basic abc header with the request. This will check the ResourceServerRetriever for the username and password sent.

Note: as per the spec - the token introspection endpoint MUST be protected by HTTPS - this means the server must be behind a TLS certificate (commonly known as SSL). Vapor OAuth leaves this up to the integrating library to implement.

Protecting Endpoints

To protect resources on other servers with OAuth using the Token Introspection endpoint, you either need to use the OAuth2TokenIntrospectionMiddleware on your routes that you want to protect, or you need to manually set up the Helper object (the middleware does this for you). Both the middleware and helper setup require:

  • tokenIntrospectionEndpoint - the endpoint where the token can be validated
  • client - the Droplet's client to send the token validation request with
  • resourceServerUsername - the username of the resource server
  • resourceServerPassword - the password of the resource server

Once either of these has been set up, you can then call request.oauth.user() or request.oauth.assertScopes() like normal.

vapor-oauth's People

Contributors

0xtim avatar vamsii777 avatar marius-se avatar brett-best avatar alexandre-pod avatar theswiftycoder avatar

Stargazers

James Fletcher avatar  avatar mynona avatar

Watchers

 avatar mynona avatar

Forkers

aquaa7 mynona

vapor-oauth's Issues

Implement OpenID Connect authentication flow

(Summary as requested by Vamsi)

1. Token Payload with the Access Token as exemplary case:

The scope in the payload of the AccessToken should be a String: where the scopes are separated by space delimeters:

scope: "openid email something"

public protocol AccessToken: JWTPayload {
    var jti: String { get } // this was a breaking change
    var clientID: String { get }
    var userID: String? { get }
    var scopes: [String]? { get }
    var expiryTime: Date { get }
}

Scopes should be returned as String? value with space delimeters. Example: "openid email something something"

To avoid breaking changes as vapor/oauth works also for simple OAuth2.0 flows without OpenID Connect, it would make sense to remove the protocol JWTPayload from AccessToken and create a new protocol for the JWTPayload.

This would give you the opportunity to use the correct claim names.

public protocol AccessTokenJWT: JWTPayload {

   var jti: String
   var aud: String // clientID
   var sub: String? // userID
   var scopes: String?
   var exp: Date // expiryTime
   var iss: String // issuer
   var iat: Date // issuedAT

// …

}

(And to make all non required claims based on the OpenIDConnect specification optional)

It is also possible to leave this completely to the consumer of vapor/oauth. I created the payload structs myself but the whole framework would be more streamlined if the JWT specification is part of it.

Up to you if you would see this valuable as part of the repository or if every consumer has to create the correct JWT payloads themselves. My reasoning is that some structured approach might actually be easier in terms of usability.

Nonce Parameter Empty When Generating ID Token

When generating an ID token in the OAuth flow, the nonce parameter is found to be empty. This issue occurs during the token generation process where the nonce, expected to be part of the request, is not being captured or passed correctly, resulting in an empty value when it is required for the ID token generation.

This behavior is inconsistent with the expected functionality, where the nonce should be correctly extracted from the request and utilized in the ID token creation. It’s crucial for maintaining the integrity and security of the OpenID Connect implementation.

Steps to Reproduce:

1.	Initiate the OAuth flow with a request that includes the nonce parameter.
2.	Proceed to the point of ID token generation.
3.	Observe that the nonce parameter is empty or not correctly passed.

Expected Behavior:
The nonce parameter should be properly extracted from the request and available during the ID token generation process.

This issue requires investigation and resolution to ensure compliance with OpenID Connect standards and the correct functioning of the OAuth flow.

Incorrect Scope Format in API Response: Single String Instead of Array for Multiple Scopes

There appears to be a discrepancy in how scopes are being handled within the Scope Validator of the vapor-oauth project. While the clientScopes are correctly retrieved as an array (e.g., ["admin", "openid"]), the scopes fetched via the API are concatenated into a single string ("admin,openid").

Specific Problem

The core issue arises from the way scopes are being processed and returned. The expected behavior is to have the scopes returned as an array of strings, which facilitates accurate scope comparisons. However, currently, the API merges all scopes into a single string, thus causing comparison mismatches and functional limitations.

Examples and Implications

  • Example 1: As demonstrated in the first screenshot, clientScopes retrieves ["admin","openid"], which is the expected format.
  • Example 2: The API response, as shown in the second screenshot, provides the scopes as "admin,openid". This format is technically correct but not suitable for direct comparisons.
  • Failure Case: This issue is exemplified in the third screenshot, where an attempted comparison of individual scopes (e.g., "openid" against "openid, admin") fails due to the string format.

The consequence of this issue is significant: it restricts users to request only one scope at a time, limiting the functionality and flexibility of scope management within the application.

Suggested Fix

The API should be modified to split the scopes string into an array before any comparison or further processing. This would align the format with the expected array structure and ensure accurate scope comparisons and validations.

Additional Context

This issue was first identified in a discussion thread (refer to the original post by @mynona in https://github.com/vamsii777/vapor-oauth/discussions/5#discussioncomment-7938901). The provided screenshots and descriptions therein offer further insights into the problem and its implications.

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.