Vertx + OpenAPI + OAuth2 security handler is calling twice - oauth-2.0

I'm a newbie in vertx world. I used this link for setting up my openapi routing: https://gist.github.com/slinkydeveloper/bdf5929c2506988d78fc08205089409a
Here is my sources:
api.yaml
#other endpoints...
/api/v1/protected/verification:
post:
summary: Summary
operationId: verification
security:
- ApiKey: []
tags:
- verification
responses:
'200':
description: User details
content:
application/json:
schema:
$ref: "#/components/schemas/User"
'401':
description: Unauthorized
default:
description: unexpected error
content:
application/json:
schema:
$ref: "#/components/schemas/Exception"
#other endpoints ....
components:
securitySchemes:
ApiKey:
type: apiKey
name: Authorization
in: header
#other ones
OpenApi setup
OpenAPI3RouterFactory.createRouterFactoryFromFile(vertx, "src/main/resources/api.yaml") { openAPI3RouterFactoryAsyncResult ->
if (openAPI3RouterFactoryAsyncResult.succeeded()) {
// Spec loaded with success
val routerFactory = openAPI3RouterFactoryAsyncResult.result()
val keycloakJson = JsonObject("{\n" +
"\"realm\": \"master\",\n" +
"\"bearer-only\": true,\n" +
"\"auth-server-url\": \"http://localhost:8080/auth\",\n" +
"\"ssl-required\": \"external\",\n" +
"\"realm-public-key\": \"MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAn9Xya697ZVZzQidld4uCwRoWmLyWBDQQhn+EL1e0WDUWq9v39OBpM+HadkYlOMvfU1A8ohGZZVBkKV4w35gkm3bFPluCPsWxdcqD1NNF6BnIC6bRicgP/4beeehff8nWI3mFAfH7Q7Ik8mm8BDQYhOPRx50JBkDiIQ7AlAjNJ+5/eIj6Pt/eZSmMSk+vM4Xu64E0mCZfHpasdf!QsFGN+VPQejNBz7h9nEdi3swIIo0ot2+5PZGELX/2Dek7cY4RMKGb+rvU6ug3UvZHQ985KuubKsWMCs8+A80yWSoA6umw1DC5rAmc5jo/6giWawuFj5jFZRx69CcMSx1VaEJ5lS4LmAi5sXuQIDAQAB\",\n" +
"\"resource\": \"vertx\",\n" +
"\"use-resource-role-mappings\": true,\n" +
"\"credentials\": {\n" +
"\"secret\": \"asd1t747-3d11-456f-a553-d8e140cfaf58\"\n" +
"}\n" +
"}")
val oauth2 = KeycloakAuth
.create(vertx, OAuth2FlowType.AUTH_CODE, keycloakJson)
val handler = OAuth2AuthHandler.create(oauth2,
"http://localhost:8081/")
routerFactory.addSecurityHandler("ApiKey", handler)
handler?.setupCallback(routerFactory.router.route("/callback"))
routerFactory.addHandlerByOperationId(EndpointUriOperationId.SIGN_UP.endpoint, { routingContext -> signUpRouter?.signUp(routingContext) })
routerFactory.addHandlerByOperationId(EndpointUriOperationId.SIGN_IN.endpoint, { routingContext -> signInRouter?.signIn(routingContext) })
routerFactory.addHandlerByOperationId(EndpointUriOperationId.CONFIRM_ACCESS_CODE.endpoint, { routingContext -> signUpRouter?.confirmAccessCode(routingContext) })
routerFactory.addHandlerByOperationId(EndpointUriOperationId.IS_EMAIL_EXISTS.endpoint, { routingContext -> signUpRouter?.isEmailExists(routingContext) })
routerFactory.addHandlerByOperationId(EndpointUriOperationId.GET_ACCESS_CODE.endpoint, { routingContext -> signUpRouter?.getAccessCode(routingContext) })
routerFactory.addHandlerByOperationId(EndpointUriOperationId.VERIFICATION.endpoint, { routingContext -> signInRouter?.verification(routingContext) })
// Add an handler with a combination of HttpMethod and path
// Before router creation you can enable or disable mounting a default failure handler for ValidationException
// Now you have to generate the router
val router = routerFactory.router
router.route().handler(CorsHandler.create("*")
.allowedMethod(HttpMethod.GET)
.allowedMethod(HttpMethod.DELETE)
.allowedMethod(HttpMethod.POST)
.allowedMethod(HttpMethod.PUT)
.allowedMethod(HttpMethod.PATCH)
.allowedHeader("Authorization")
.allowedHeader("user-agent")
.allowedHeader("Access-Control-Request-Method")
.allowedHeader("Access-Control-Allow-Credentials")
.allowedHeader("Access-Control-Allow-Origin")
.allowedHeader("Access-Control-Allow-Headers")
.allowedHeader("Content-Type")
.allowedHeader("Content-Length")
.allowedHeader("X-Requested-With")
.allowedHeader("x-auth-token")
.allowedHeader("Location")
.exposedHeader("Location")
.exposedHeader("Content-Type")
.exposedHeader("Content-Length")
.exposedHeader("ETag")
.maxAgeSeconds(60))
// Now you can use your Router instance
val server = vertx.createHttpServer(HttpServerOptions().setPort(8081).setHost("0.0.0.0"))
server.requestHandler(router::accept).listen()
print("server is run\n")
} else {
// Something went wrong during router factory initialization
val exception = openAPI3RouterFactoryAsyncResult.cause()
print("OpenApiRoutingError! $exception")
}
}
My verification endpoint
fun verification(routingContext: RoutingContext) {
val map = (routingContext.user() as AccessTokenImpl).accessToken().map
val username = map.get("preferred_username").toString()
vertx.eventBus().send(SignInHandlerChannel.VERIFICATION.value(), username, { reply: AsyncResult<Message<String>> ->
if (reply.succeeded()) {
val result = JsonUtils.toPojo<MultiPosResponse<VerificationMapper>>(reply.result().body().toString())
routingContext
.response()
.putBrowserHeaders()
.setStatusCode(result.code!!)
.end(result.toJson())
} else{
routingContext
.response()
.putBrowserHeaders()
.setStatusCode(HttpResponseStatus.INTERNAL_SERVER_ERROR.code())
.end(reply.cause().toString())
}
})
}
The problem is when I send request to "/api/v1/protected/verification" endpoint my oauth2 handler works twice. First request processes by oauth handler(as must be) then passed to next, to my endpoint handler after that for some reason request passed to oauth2 handler again(as I think it shouldn't be). How can I fix this problem?

Related

Headers are not being shown in Swagger API documentation for Ktor API (Kotlin)

I'm using the papsign/Ktor-OpenAPI-Generator to generate Swagger API documentation for a Ktor application. I have a POST endpoint which contains headers in the request. Here is the entire code:
fun main(args: Array<String>): Unit =
io.ktor.server.netty.EngineMain.main(args)
#Suppress("unused")
fun Application.module() {
install(OpenAPIGen) {
info {
version = "1.0.0"
title = "Accelerated Earn"
description = "Accelerated earn service provides extra earn."
contact {
name = "John Doe"
email = "johndoe#gmail.com"
}
}
// describe the servers, add as many as you want
server("/") {
description = "This server"
}
replaceModule(DefaultSchemaNamer, object: SchemaNamer {
val regex = Regex("[A-Za-z0-9_.]+")
override fun get(type: KType): String {
return type.toString().replace(regex) { it.value.split(".").last() }.replace(Regex(">|<|, "), "_")
}
})
}
// Configuring jackson serialization
install(ContentNegotiation) {
jackson {
dateFormat = DateFormat.getDateTimeInstance()
}
}
// Configuring swagger routes
routing {
get("/openapi.json") {
call.respond(application.openAPIGen.api.serialize())
}
get("/") {
call.respondRedirect("/swagger-ui/index.html?url=/openapi.json", true)
}
}
demoFunction()
}
// Defining the DEMO tag
enum class Tags(override val description: String) : APITag {
DEMO("This is a demo endpoint")
}
// Request is of type DemoClass, response is Boolean
fun Application.demoFunction() {
apiRouting {
tag(Tags.DEMO) {
route("/demo") {
post<Unit, Boolean, DemoClass>(
status(HttpStatusCode.OK),
info(
"Demo endpoint",
"This is a demo API"
),
exampleResponse = true,
exampleRequest = DemoClass(name = "john doe", xyz = "some header")
) { _, request ->
respond(true)
}
}
}
}
}
#Request("Request")
data class DemoClass(
#HeaderParam("Sample header")
val xyz: String,
#PathParam("Name")
val name: String
)
In DemoClass, I have used #HeaderParam annotation to denote "xyz" property as a header, and used #PathParam annotation to denote "name" as a parameter of the request. I expected that "xyz" would be shown as a header in the documentation but it is being shown as a part of the request body, and nothing about headers is mentioned (as shown in the below figures)
Because of this, while making a request, I have to put the header inside the request body instead of passing it as a header in the request. Is it possible to fix this? How do I do it?
Found the error. The first parameter of post function is the class of header. Here is the updated code :-
fun Application.demoFunction() {
apiRouting {
tag(Tags.DEMO) {
route("/demo") {
post<Headers, Boolean, DemoClass>(
status(HttpStatusCode.OK),
info(
"Demo endpoint",
"This is a demo API"
),
exampleResponse = true,
exampleRequest = DemoClass(name = "john doe")
) { header, request ->
respond(true)
}
}
}
}
}
#Request("Request")
data class DemoClass(
#PathParam("Name")
val name: String
)
data class Headers(
#HeaderParam("Sample Header")
val xyz: String
)

EMQX http ACL auth - broker isn't available

I use EMQ X Broker v4.0.1. Simple http auth is work fine, but when I try to use http ACL auth - it doesn't work for me, despite the fact that settings are very close. When I try to refer to the broker via Eclipse Paho I get the error with status code 3 that means the broker isn't available. I turned on emqx_auth_http from dashboard. This is my EMQX settings for http ACL auth:
emqx.conf
listener.tcp.external = 1884
plugins/emqx_auth_http.conf
auth.http.auth_req = http://127.0.0.1:8991/mqtt/auth
auth.http.auth_req.method = post
auth.http.auth_req.params = clientid=%c,username=%u,password=%P
auth.http.super_req = http://somesite.com/mqtt/superuser
auth.http.super_req.method = post
auth.http.super_req.params = clientid=%c,username=%u
auth.http.acl_req = http://somesite/mqtt/acl
auth.http.acl_req.method = post
auth.http.acl_req.params = access=%A,username=%u,clientid=%c,ipaddr=%a,topic=%t,mountpoint=%m
auth.http.request.retry_times = 3
auth.http.request.retry_interval = 1s
auth.http.request.retry_backoff = 2.0
Endpoints(http://somesite.com/mqtt/superuser, http://somesite/mqtt/acl) are working fine and I can get access to it from Postaman app. May be you could tell me where I do something wrong in my configuration or somewhere else?
Maybe uou need to provide your HTTP server code.
http respose status 200 is ok
http respose status 4xx is unauthorized
http respose status 200 and body is ignore means break
This is a project just passed the test:
egg-iot-with-mqtt
/**
* Auth
*/
router.post('/mqtt/auth', async (ctx, next) => {
const { clientid, username, password } = ctx.request.body
// Mock
// 200 means ok
if (clientid === '' || 'your condition') {
ctx.body = ''
} else {
// 4xx unauthorized
ctx.status = 401
}
})
/**
* ACL
*/
router.post('/mqtt/acl', async (ctx, next) => {
/**
* Request Body
* access: 1 | 2, 1 = sub, 2 = pub
* access in body now is string !!!
{
access: '1',
username: 'undefined',
clientid: 'mqttjs_bf980bf7',
ipaddr: '127.0.0.1',
topic: 't/1',
mountpoint: 'undefined'
}
*/
const info = ctx.request.body
console.log(info)
if (info.topic === 't/2') {
// 200 is ok
ctx.body = ''
} else {
// 4xx is unauthorized
ctx.status = 403
}
})

Display only endpoints available to user in Swagger after his login

I would like to setup the follownig workflow:
Initially, without login, Swagger shows only 2-3 endpoints - this will be done by providing limited openapi3 json from backend, no problem;
User logs in via Authorize button (works, openapi3 json has necessary info);
After login, Swagger emits one more request with user credentials, backend provides new openapi3 json with endpoints available to this specific user and Swagger redraws the page with new data. Preferably, user is still logged in.
Is it possible to do Item 3 with Swagger? How can I manually emit request from Swagger with OAuth2 bearer token (since user logged, token must present somwhere) and redraw Swagger page?
The task was done via Swagger customization using its plugin system.
Actually Swagger is a JavaScript (Babel, Webpack) project using React / Redux and it was a little bit hard to dig into it since I do not know React (my tool is Python) but finally I managed.
Here is the code for my custom plugin with comments:
const AuthorizedPlugin = function(system) {
return {
statePlugins: {
auth: { // namespace for authentication subsystem
// last components invoked after authorization or logout are
// so-called reducers, exactly they are responsible for page redraw
reducers: {
"authorize_oauth2": function(state, action) {
let { auth, token } = action.payload
let parsedAuth
auth.token = Object.assign({}, token)
parsedAuth = system.Im.fromJS(auth)
var req = {
credentials: 'same-origin',
headers: {
accept: "application/json",
Authorization: "Bearer " + auth.token.access_token
},
method: 'GET',
url: system.specSelectors.url()
}
// this is the additional request with token I mentioned in the question
system.fn.fetch(req).then(
function (result) {
// ... and we just call updateSpec with new openapi json
system.specActions.updateSpec(result.text)
}
)
// This line is from the original Swagger-ui code
return state.setIn( [ "authorized", parsedAuth.get("name") ], parsedAuth )
},
"logout": function(state, action) {
var req = {
credentials: 'same-origin',
headers: { accept: "application/json" },
method: 'GET',
url: system.specSelectors.url()
}
// for logout, request does not contain authorization token
system.fn.fetch(req).then(
function (result) {
system.specActions.updateSpec(result.text)
}
)
// these lines are to make lock symbols gray and remove credentials
var result = state.get("authorized").withMutations(function (authorized) {
action.payload.forEach(function (auth) {
authorized.delete(auth);
});
});
return state.set("authorized", result)
}
}
}
}
}
}
Insert this plugin as usual:
const ui = SwaggerUIBundle({{
url: '{openapi_url}',
dom_id: '#swagger-ui',
defaultModelsExpandDepth: -1,
displayOperationId: false,
presets: [
SwaggerUIBundle.presets.apis,
SwaggerUIBundle.SwaggerUIStandalonePreset
],
plugins: [
AuthorizedPlugin
],
layout: "BaseLayout",
deepLinking: true
})

Use MSAL in Microsoft Teams not work JS

I work on an MS Teams app with a connection to MS Graph.
I'm trying to get an access token for MS Graph in MS Teams. To get a token I'm using MSAL js.
If I run the App with gulp serve I receive a valid token and I have access to the MS Graph endpoints. But if I build the app and install it in MS Teams the function userAgentApplication.acquireTokenSilent(config) is never executed. I tested it with a console.log before and after the call. There is no error thrown.
Do you have any idea why the above snippet is not executed in MS Teams (app and webapp)?
NEW:
On Home:
export function login() {
const url = window.location.origin + '/login.html';
microsoftTeams.authentication.authenticate({
url: url,
width: 600,
height: 535,
successCallback: function(result: string) {
console.log('Login succeeded: ' + result);
let data = localStorage.getItem(result) || '';
localStorage.removeItem(result);
let tokenResult = JSON.parse(data);
storeToken(tokenResult.accessToken)
},
failureCallback: function(reason) {
console.log('Login failed: ' + reason);
}
});
}
On Login
microsoftTeams.initialize();
// Get the tab context, and use the information to navigate to Azure AD login page
microsoftTeams.getContext(function (context) {
// Generate random state string and store it, so we can verify it in the callback
let state = _guid();
localStorage.setItem("simple.state", state);
localStorage.removeItem("simple.error");
// See https://learn.microsoft.com/en-us/azure/active-directory/develop/active-directory-v2-protocols-implicit
// for documentation on these query parameters
let queryParams = {
client_id: "XXX",
response_type: "id_token token",
response_mode: "fragment",
scope: "https://graph.microsoft.com/User.Read openid",
redirect_uri: window.location.origin + "/login-end.html",
nonce: _guid(),
state: state,
login_hint: context.loginHint,
};
// Go to the AzureAD authorization endpoint (tenant-specific endpoint, not "common")
// For guest users, we want an access token for the tenant we are currently in, not the home tenant of the guest.
let authorizeEndpoint = `https://login.microsoftonline.com/${context.tid}/oauth2/v2.0/authorize?` + toQueryString(queryParams);
window.location.assign(authorizeEndpoint);
});
// Build query string from map of query parameter
function toQueryString(queryParams) {
let encodedQueryParams = [];
for (let key in queryParams) {
encodedQueryParams.push(key + "=" + encodeURIComponent(queryParams[key]));
}
return encodedQueryParams.join("&");
}
// Converts decimal to hex equivalent
// (From ADAL.js: https://github.com/AzureAD/azure-activedirectory-library-for-js/blob/dev/lib/adal.js)
function _decimalToHex(number) {
var hex = number.toString(16);
while (hex.length < 2) {
hex = '0' + hex;
}
return hex;
}
// Generates RFC4122 version 4 guid (128 bits)
// (From ADAL.js: https://github.com/AzureAD/azure-activedirectory-library-for-js/blob/dev/lib/adal.js)
function _guid() {...}
on login end
microsoftTeams.initialize();
localStorage.removeItem("simple.error");
let hashParams = getHashParameters();
if (hashParams["error"]) {
// Authentication/authorization failed
localStorage.setItem("simple.error", JSON.stringify(hashParams));
microsoftTeams.authentication.notifyFailure(hashParams["error"]);
} else if (hashParams["access_token"]) {
// Get the stored state parameter and compare with incoming state
let expectedState = localStorage.getItem("simple.state");
if (expectedState !== hashParams["state"]) {
// State does not match, report error
localStorage.setItem("simple.error", JSON.stringify(hashParams));
microsoftTeams.authentication.notifyFailure("StateDoesNotMatch");
} else {
// Success -- return token information to the parent page.
// Use localStorage to avoid passing the token via notifySuccess; instead we send the item key.
let key = "simple.result";
localStorage.setItem(key, JSON.stringify({
idToken: hashParams["id_token"],
accessToken: hashParams["access_token"],
tokenType: hashParams["token_type"],
expiresIn: hashParams["expires_in"]
}));
microsoftTeams.authentication.notifySuccess(key);
}
} else {
// Unexpected condition: hash does not contain error or access_token parameter
localStorage.setItem("simple.error", JSON.stringify(hashParams));
microsoftTeams.authentication.notifyFailure("UnexpectedFailure");
}
// Parse hash parameters into key-value pairs
function getHashParameters() {
let hashParams = {};
location.hash.substr(1).split("&").forEach(function (item) {
let s = item.split("="),
k = s[0],
v = s[1] && decodeURIComponent(s[1]);
hashParams[k] = v;
});
return hashParams;
}
Even though my answer is quite late, but I faced the same problem recently.
The solution is farely simple: MSALs silent login does not work in MSTeams (yet) as MSTeams relies on an IFrame Approach that is not supported by MSAL.
You can read all about it in this Github Issue
Fortunately, they are about to release a fix for this in Version msal 1.2.0 and there is already an npm-installable beta which should make this work:
npm install msal#1.2.0-beta.2
Update: I tried this myself - and the beta does not work as well. This was confirmed by Microsoft in my own Github Issue.
So I guess at the time being, you simply can't use MSAL for MS Teams.

Node.js - Array is converted to object when sent in HTTP GET request query

The following Node.js code:
var request = require('request');
var getLibs = function() {
var options = { packages: ['example1', 'example2', 'example3'], os: 'linux', pack_type: 'npm' }
request({url:'http://localhost:3000/package', qs:options},
function (error , response, body) {
if (! error && response.statusCode == 200) {
console.log(body);
} else if (error) {
console.log(error);
} else{
console.log(response.statusCode);
}
});
}();
sends the following http GET request query that is received by like this:
{"packages"=>{"0"=>"example1", "1"=>"example2", "2"=>"example3"}, "os"=>"linux", "pack_type"=>"npm"}
How can I optimize this request to be received like this:
{"packages"=>["example1", "example2", "example3"], "os"=>"linux", "pack_type"=>"npm"}
Note. The REST API is built in Ruby on Rails
If the array need to be received as it is, you can set useQuerystring as true:
UPDATE: list key in the following code example has been changed to 'list[]', so that OP's ruby backend can successfully parse the array.
Here is example code:
const request = require('request');
let data = {
'name': 'John',
'list[]': ['XXX', 'YYY', 'ZZZ']
};
request({
url: 'https://requestb.in/1fg1v0i1',
qs: data,
useQuerystring: true
}, function(err, res, body) {
// ...
});
In this way, when the HTTP GET request is sent, the query parameters would be:
?name=John&list[]=XXX&list[]=YYY&list[]=ZZZ
and the list field would be parsed as ['XXX', 'YYY', 'ZZZ']
Without useQuerystring (default value as false), the query parameters would be:
?name=John&list[][0]=XXX&list[][1]=YYY&list[][2]=ZZZ
I finally found a fix. I used 'qs' to stringify 'options' with {arrayFormat : 'brackets'} and then concatinated to url ended with '?' as follows:
var request = require('request');
var qs1 = require('qs');
var getLibs = function() {
var options = qs1.stringify({
packages: ['example1', 'example2', 'example3'],
os: 'linux',
pack_type: 'npm'
},{
arrayFormat : 'brackets'
});
request({url:'http://localhost:3000/package?' + options},
function (error , response, body) {
if (! error && response.statusCode == 200) {
console.log(body);
} else if (error) {
console.log(error);
} else{
console.log(response.statusCode);
}
});
}();
Note: I tried to avoid concatenation to url, but all responses had code 400
This problem can be solved using Request library itself.
Request internally uses qs.stringify. You can pass q option to request which it will use to parse array params.
You don't need to append to url which leaves reader in question why that would have been done.
Reference: https://github.com/request/request#requestoptions-callback
const options = {
method: 'GET',
uri: 'http://localhost:3000/package',
qs: {
packages: ['example1', 'example2', 'example3'],
os: 'linux',
pack_type: 'npm'
},
qsStringifyOptions: {
arrayFormat: 'repeat' // You could use one of indices|brackets|repeat
},
json: true
};

Resources