Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor inbound authentication with custom provider and handlers #15056

Merged
merged 56 commits into from
May 3, 2019

Conversation

ldclakmal
Copy link
Member

@ldclakmal ldclakmal commented Apr 17, 2019

Purpose

This PR provides the capability of custom authentication providers and handlers engagement for inbound authentication. Currently, the user cannot attach a custom provider and handler because the authentication filter related logic is tightly coupled with the pre-provided handlers by Ballerina.

Fixes #14823
Fixes #15175

Approach

  • ballerina/auth module provides an abstract object named AuthProvider, which authenticate the provided credentials and return the status of authentication as true or false. ConfigAuthStoreProvider, JWTAuthProvider and LdapAuthStoreProvider are implementations of the AuthProvider for different use cases.
  • At the authentication phase of the auth-provider, it is a must to get the supported scopes and assign to runtime:Principal.scopes, since it is used at the authorization phase.

  • auth package of ballerina/http module has the authentication filter, authorization filter, and authentication handlers.
  • ballerina/http module provides an abstract object named AuthnHandler, which handle the authentication of the provided http request and return the status of authentication as true or false. BasicAuthnHandler and JwtAuthnHandler are implementations of the AuthnHandler for different use cases.

If a user wants to engage a custom authentication logic, it is needed to write a custom provider and handler as follows. Or else already implemented handlers and providers can be used.

Custom Provider

import ballerina/auth;

public type CustomProvider object {

    *auth:AuthProvider;

    public function authenticate(string credential) returns boolean|error {
        // Logic related to authenticate the provided credentials
    }
}

Custom Handler

import ballerina/auth;
import ballerina/http;

public type CustomHandler object {

    *http:AuthnHandler;

    public auth:AuthProvider authProvider;
    public function __init(auth:AuthProvider authProvider) {
        self.authProvider = authProvider;
    }
};

public function CustomHandler.handle(http:Request req) returns boolean|error {
    // Logic related to handling the provided HTTP request
}

public function CustomHandler.canHandle(http:Request req) returns boolean {
    // Logic related to the capability of handling the provided HTTP request
}

Samples

Sample 1 - Basic authn handler example

This is a sample program which handles authentication with authorization with Basic auth.

import ballerina/auth;
import ballerina/http;

auth:ConfigAuthStoreProvider basicAuthProvider = new;
http:BasicAuthnHandler basicAuthnHandler = new(basicAuthProvider);

listener http:Listener listener = new(9090, config = {
    auth: {
        authnHandlers: [basicAuthnHandler],
        scopes: ["scopes-1", "scopes-2"]
    },
    secureSocket: {
        keyStore: {
            path: "${ballerina.home}/bre/security/ballerinaKeystore.p12",
            password: "ballerina"
        }
    }
});

service echo on listener {
    resource function test(http:Caller caller, http:Request req) {
        checkpanic caller->respond(());
    }
}

Sample 2 - JWT authn handler example

This is a sample program which handles authentication with authorization with JWT.

import ballerina/auth;
import ballerina/http;

auth:JWTAuthProvider jwtAuthProvider = new({
    issuer: "example1",
    audience: ["ballerina"],
    certificateAlias: "ballerina",
    trustStore: {
        path: "${ballerina.home}/bre/security/ballerinaTruststore.p12",
        password: "ballerina"
    }
});

http:JwtAuthnHandler jwtAuthnHandler = new(jwtAuthProvider);

listener http:Listener listener = new(9090, config = {
    auth: {
        authnHandlers: [jwtAuthnHandler],
        scopes: ["scopes-1", "scopes-2"]
    },
    secureSocket: {
        keyStore: {
            path: "${ballerina.home}/bre/security/ballerinaKeystore.p12",
            password: "ballerina"
        }
    }
});

service echo on listener {
    resource function test(http:Caller caller, http:Request req) {
        checkpanic caller->respond(());
    }
}

Sample 3 - Basic authn handler with config overwrite example

Here, listener config will be overwritten by service level config and then service level config will be overwritten by resource level config.

import ballerina/auth;
import ballerina/http;

auth:ConfigAuthStoreProvider basicAuthProvider = new;
http:BasicAuthnHandler basicAuthnHandler1 = new(basicAuthProvider);
http:BasicAuthnHandler basicAuthnHandler2 = new(basicAuthProvider);
http:BasicAuthnHandler basicAuthnHandler3 = new(basicAuthProvider);

listener http:Listener listener = new(9090, config = {
    auth: {
        authnHandlers: [basicAuthnHandler1],
        scopes: ["scopes-1", "scopes-2"]
    },
    secureSocket: {
        keyStore: {
            path: "${ballerina.home}/bre/security/ballerinaKeystore.p12",
            password: "ballerina"
        }
    }
});

@http:ServiceConfig {
    auth: {
        enabled: true,
        authnHandlers: [basicAuthnHandler2],
        scopes: ["scopes-3", "scopes-4"]
    }
}
service echo on listener {

    @http:ResourceConfig {
        auth: {
            enabled: true,
            authnHandlers: [basicAuthnHandler3],
            scopes: ["scope-5", "scope-6"]
        }
    }
    resource function test(http:Caller caller, http:Request req) {
        checkpanic caller->respond(());
    }
}

Sample 4 - Custom authn handler example

This is a sample program which handles authentication with authorization with a custom header.

sample.bal

import ballerina/auth;
import ballerina/http;
import ballerina/log;

CustomAuthStoreProvider customAuthStoreProvider = new;
CustomAuthnHandler customAuthnHandler = new(customAuthStoreProvider);

listener http:Listener ep = new(9090, config = {
        auth: {
            authnHandlers: [customAuthnHandler],
            scopes: ["all"]
        },
        secureSocket: {
            keyStore: {
                path: "${ballerina.home}/bre/security/ballerinaKeystore.p12",
                password: "ballerina"
            }
        }
    });

service hello on ep {
    resource function sayHello(http:Caller caller, http:Request req) {
        checkpanic caller->respond("Hello Ballerina!");
    }
}

custom_authn_handler.bal

import ballerina/auth;
import ballerina/http;
import ballerina/log;

public type CustomAuthnHandler object {

    *http:AuthnHandler;

    public auth:AuthProvider authProvider;

    public function __init(auth:AuthProvider authProvider) {
        self.authProvider = authProvider;
    }
};

public function CustomAuthnHandler.handle(http:Request req) returns boolean|error {
    var customAuthHeader = req.getHeader(http:AUTH_HEADER);
    string credential = customAuthHeader.substring(6, customAuthHeader.length()).trim();
    var authenticated = self.authProvider.authenticate(credential);
    if (authenticated is boolean) {
        return authenticated;
    }
    return false;
}

public function CustomAuthnHandler.canHandle(http:Request req) returns boolean {
    var customAuthHeader = req.getHeader(http:AUTH_HEADER);
    return customAuthHeader.hasPrefix("Custom");
}

custom_auth_store_provider.bal

import ballerina/auth;
import ballerina/encoding;
import ballerina/runtime;

public type CustomAuthStoreProvider object {

    *auth:AuthProvider;

    public function authenticate(string credential) returns boolean|error {
        string actualUsername = "abc";
        string actualPassword = "123";

        string decodedHeaderValue = encoding:byteArrayToString(check encoding:decodeBase64(credential));
        string[] decodedCredentials = decodedHeaderValue.split(":");
        string username = decodedCredentials[0];
        string password = decodedCredentials[1];

        boolean isAuthenticated = username == actualUsername && password == actualPassword;
        if (isAuthenticated) {
            runtime:Principal principal = runtime:getInvocationContext().principal;
            principal.userId = username;
            principal.username = username;
            principal.scopes = self.getScopes();
        }
        return isAuthenticated;
    }

    public function getScopes() returns string[] {
        string[] scopes = ["all"];
        return scopes;
    }
};

Invoke the service with following curl command.
$ curl -kv https://localhost:9090/hello/sayHello -H "Authorization: Custom YWJjOjEyMw=="

Testing

  • Authentication ( 3x2x2 = 12 scenarios)
Auth Header Status (Service, Resource) Auth Enabled Response Code
No Auth Headers [T,T], [T,F], [F,T], [F,F] 401, 200, 401, 200
Valid Auth Headers [T,T], [T,F], [F,T], [F,F] 200, 200, 200, 200
Invalid Auth Headers [T,T], [T,F], [F,T], [F,F] 401, 200, 401, 200
  • Authorization ( 3x3x3 = 27 scenarios)
(Listener, Service, Resource) Scopes Status Response Code
[✔,✔,✔], [✔,✔,✗], [✔,✔,━], [✔,✗,✔], [✔,✗,✗], [✔,✗,━], [✔,━,✔], [✔,━,✗], [✔,━,━] 200, 403, 200, 200, 403, 403, 200, 403, 200
[✗,✔,✔], [✗,✔,✗], [✗,✔,━], [✗,✗,✔], [✗,✗,✗], [✗,✗,━], [✗,━,✔], [✗,━,✗], [✗,━,━] 200, 403, 200, 200, 403, 403, 200, 403, 403
[━,✔,✔], [━,✔,✗], [━,✔,━], [━,✗,✔], [━,✗,✗], [━,✗,━], [━,━,✔], [━,━,✗], [━,━,━] 200, 403, 200, 200, 403, 403, 200, 403, 200

✔ - Valid scopes
✗ - Invalid scopes
━ - Scopes not given

Check List

  • Read the Contributing Guide
  • Required Balo version update
  • Updated Change Log
  • Checked Tooling Support (Need tooling support for PR #15056 #15175)
  • Added necessary tests
    • Unit Tests
    • Spec Conformance Tests
    • Integration Tests
    • Ballerina By Example Tests
  • Increased Test Coverage
  • Added necessary documentation
    • API documentation
    • Module documentation in Module.md files
    • Ballerina By Examples

@@ -0,0 +1,26 @@
// Copyright (c) 2018 WSO2 Inc. (http://www.wso2.org) All Rights Reserved.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this a new file? Incorrect year

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Earlier this file was auth_store_provider.bal. In this PR I have renamed it and updated.

Copy link
Contributor

@nadeeshaan nadeeshaan left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed changes affected to Language server

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Team/StandardLibs All Ballerina standard libraries Type/Improvement
Projects
None yet
9 participants