OIDC - Extra parameters in sign-in URL query string - javascript

I have a Javascript client which uses OIDC for authentication. I'm using the authorization code flow. Here is a code snippet:
var config = {
authority: "http://localhost:5000",
client_id: "js",
redirect_uri: "http://localhost:5003/callback.html",
response_type: "code",
scope:"openid profile web_api",
post_logout_redirect_uri: "http://localhost:5003/index.html"
};
var mgr = new Oidc.UserManager(config);
I would like to be able to add extra parameters in the config object above which would be available in the query string of the URL that I have access to in the Login method of my Authorization Server (http://localhost:5000/Account/Login):
(C# code):
// <summary>
/// Entry point into the login workflow
/// </summary>
[HttpGet]
public async Task<IActionResult> Login(string returnUrl)
{
...
}
(I can access the URL query string in the code above by both the returnUrl parameter or the HttpContext.Request.Query property)
Unfortunately, if I set new (non-standard) parameters in the config object on the Javascript client, their are not passed to the URL query string.
Contextualizing: The reason I need this feature is because there are extra parameters that are mandatory for me to authenticate the user, besides username and password. However, these parameters are not explicitly informed by the user. They have their values assigned inside the client Javascript code (Ex: the device ID (like a cell phone's IMEI) of the client). If there is any other easier way to achieve this, I would be glad to know about.
I'm able to achieve this using Postman, based on this discussion on GitHub:
Because in Postman you can change the authorization endpoint URL to:
http://MyAuthorizationEndpoint?paramName=paramValue
Ex: http://localhost:5000/connect/authorize?device_id=XYZ
But I'm not able to do this in the Javascript client because I do not specify the authorization endpoint explicitly, only the authority (as seen in the config object above).
OBS: I don't intend to use any other type of authorization flow, like using an Extension Grant, since it's more insecure and not recommended.

Found the solution after reading this discussion. Method Oidc.UserManager.signinRedirect accepts an extraQueryParams argument that has this exact purpose:
(Javascript client):
mgr.signinRedirect({
extraQueryParams: {
device_id: "XYZ"
},
});
This is very useful for anyone searching for a solution for Authorization Code Flow in which you need to pass custom parameters for validation before issuing the code/token.

Related

Microsoft Graph API token validation failure

I would use Microsoft Graph API in my Angular Web application.
First I make connexion using msal library
When I try log in with my profil I get this error
I have configured my app as the mentionned in the official git sample
MsalModule.forRoot({
clientID: "Tenant ID",
authority: "https://login.microsoftonline.com/common/",
redirectUri: "http://localhost:4200/",
validateAuthority : true,
popUp: true
}),
Authetification is working and I get the token.
Then when I'm in home page I make a second request to Microsoft Graph API to get user information using that token.
getProfile() {
let header= new Headers();
let tokenid= sessionStorage.getItem('msal.idtoken');
header.set('Authorization', 'Bearer ' + tokenid)
let url ="https://graph.microsoft.com/v1.0/me/"
return this.http.get(url,{headers:header});
}
}
I get an 401 Unauthorized error with a response :
{
"error": {
"code": "InvalidAuthenticationToken",
"message": "Access token validation failure.",
"innerError": {
"request-id": "xxxxxx",
"date": "2018-10-09T22:58:41"
}
}
}
I don't know why MG API is not accepting my token, Am I using wrong authority url ?
UPDATE: I have understood that actually I get id_token which is different from access token. How can I get Access token from MSAL library to make MS GRAPH API calls ?:
According to the same sample you can also attach an HttpInterceptor that will automatically attach the access token to each (external) HTTP call.
By reading through the documentation I found the following information.
consentScopes: Allows the client to express the desired scopes that should be consented. Scopes can be from multiple resources/endpoints. Passing scope here will only consent it and no access token will be acquired till the time client actually calls the API. This is optional if you are using MSAL for only login (Authentication).
That suggests that using the HttpInterceptor doesn't only attach the access token, but also retrieves it. The token that you're seeing is probably just a token for your application, but isn't a valid token for the Graph API.
Internally it uses getCachedTokenInternal(scopes: Array<string>, user: User) to get a new access token for specific scopes code found here. I'm not sure if you can use this method as well to get a new token for that resource. I would just use the interceptor.
You could try to copy the access token and see how it looks like on jwt.ms (a Microsoft provided JWT token viewer) or jwt.io.
Any tokens valid for Graph should have the Audience of https://graph.microsoft.com, so if you inspect the token (in jwt.ms) it should at least have this value.
"aud": "https://graph.microsoft.com",
The issue is that you're using the id_token instead of the access token:
let tokenid= sessionStorage.getItem('msal.idtoken');
becomes something like:
let tokenid= sessionStorage.getItem('msal.token'); // or msal.accesstoken
Update(per Phillipe's comment)
You need to select the scopes that you want to target in your application. So, it looks like you want the user profile, so you'll want to add the consentScopes property to specify which scopes your app will use:
MsalModule.forRoot({
clientID: "Tenant ID",
authority: "https://login.microsoftonline.com/common/",
redirectUri: "http://localhost:4200/",
validateAuthority : true,
popUp: true,
consentScopes: ["user.read"]
}),
Make sure you add your endpoint to Resource Map configuration. See this link: https://github.com/AzureAD/microsoft-authentication-library-for-js/tree/dev/samples/MSALAngularDemoApp
export const protectedResourceMap:[string, string[]][]=[ ['https://graph.microsoft.com/v1.0/me', ['user.read']] ];

Q: Google Photos Library API - I don't know how it works, someone?

I'm trying to load an album from Google Photos via javascript but I don't understand how the api works, I started reading Google Photos API but no luck. Is there a code reference that I can follow to get a list of the photos of my album?
I found this but doesn't work
<script>
var scopeApi = ['https://www.googleapis.com/auth/photoslibrary', 'https://www.googleapis.com/auth/photoslibrary.readonly', 'https://www.googleapis.com/auth/photoslibrary.readonly.appcreateddata'];
function onAuthPhotoApiLoad() {
window.gapi.auth.authorize(
{
'apiKey': 'MY_API_KEY',
'client_id': "MY_CLIEND_ID",
'scope': scopeApi,
'immediate': false
},
handlePhotoApiAuthResult);
}
function handlePhotoApiAuthResult(authResult) {
if (authResult && !authResult.error) {
oauthToken = authResult.access_token;
GetAllPhotoGoogleApi();
}
}
function GetAllPhotoGoogleApi() {
gapi.client.request({
'path': 'https://photoslibrary.googleapis.com/v1/albums',
'method': 'POST'
}).then(function (response) {
console.log(response);
}, function (reason) {
console.log(reason);
});
}
onAuthPhotoApiLoad();
While in the process of developing a Photos synching script, I spent a few days researching and testing the Oauth 2.0 documentation. It's a lot to take in, but hopefully this Cliff-notes version is helpful:
App Setup You first need to get an application configuration through the developer console at console.developers.google.com/ and make sure that the Photos data is shared.
You'll get a JSON file that looks like this
{"installed":{
"client_id":"xxxxxxxxxxxxxxxxxxxx.apps.googleusercontent.com",
"project_id":"xxxx-xxxxxxxx-123456",
"auth_uri":"https://accounts.google.com/o/oauth2/auth",
"token_uri":"https://accounts.google.com/o/oauth2/token",
"auth_provider_x509_cert_url":"https://www.googleapis.com/oauth2/v1/certs",
"client_secret":"xxxxxxxxxxxxxxxxxxxxxxxx",
"redirect_uris":["urn:ietf:wg:oauth:2.0:oob","http://localhost"]
}}
Request Authorization Code - You then need to write code that uses those values to get an authorization token - basically a string that indicates the user has allowed your application access to their data.
Send a request to the auth_uri endpoint with these values in the querystring:
scope - a space-delimited list of scopes from developers.google.com/photos that says you want your user to grant access to these features
redirect_uri - a URL you own that can capture an incoming querystring
client_id - from your developer config in step 1
state - 32 random bytes, base64 encoded and made URL-friendly by replacing "+","/","=" with "-","_","" respectively
code_challenge - a SHA256 hash of another 32 random bytes, base64 encoded and made URL-friendly
code_challenge_method - "S256" (no quotes)
Authorization round trip Sending this composed URI to a user's browser will allow them to choose a Google account and show which scopes are being requested. Once that form is submitted, it will redirect to your redirect_uri with querystring (Method = GET) values:
code - the authorization code you can use to request an access token
state - a string you can use to validate against your hash
Get an access_token Finally you exchange the authorization code for an OAuth AccessToken that you'll put in the HTTP header of all the API requests. The request goes to the token_uri from step 1 and has these request body (Method = POST) parameters:
code - you got from the redirect querystring in Step 3
redirect_uri - same as above, but this may not be used
client_id - from configuration
code_verifier - code_challenge before it was hashed
client_secret - from configuration
scope - can be empty here
grant_type - "authorization_code" (no quotes)
Use the access tokens The response from that request will have an access_token and a refresh_token. You can use the short-lived access_token immediately in your API request's HTTP header. Store the long-lived refresh_token so you can get a new access_token without authorizing again.
That's the gist of it. You can look at my Powershell script for an example of the authorization and authentication flows which work even though the rest is a little buggy and incomplete. Paging through albums is getting a 401 error sometimes.

Test authenticated Cloud Endpoints methods offline

I'm going on a long flight tomorrow and I'd like to be able to keep testing my cloud endpoints REST API while offline. The problem is that the User object is integral to most of my methods, and I need an internet connection to create valid OAuth tokens to call them from the client side (JavaScript).
On the Dev server though, no matter what account you log in on, the user is always the same (with email example#example.com). But if you feed it bogus tokens, it throws an OAuthRequestException.
Is there any way I can generate valid test tokens offline for the dev server or a way to access the User object without providing tokens at all?
Here's an example of a method I'd like to test while offline:
#ApiMethod(name = "hylyts.get")
public Hylyt getHylyt(#Named("url") String url, #Named("id") long id, User user)
throws OAuthRequestException, UnauthorizedException {
return ofy().load().type(Hylyt.class).parent(util.getArticleKey(url, user)).id(id).now();
}
There's a little documented way to inject a custom Authenticator class in Cloud Endpoints. This allows you to change the way the User is detected.
Here's how it works :
#Api(name = "myapi", version = "v1", authenticators = {MyDummyAuthenticator.class})
public class MyAPI {
#ApiMethod(name = "hylyts.get")
public Hylyt getHylyt(#Named("url") String url, #Named("id") long id, User user)
throws OAuthRequestException, UnauthorizedException {
return ofy().load().type(Hylyt.class).parent(util.getArticleKey(url, user)).id(id).now();
}
}
And here's what your Authenticator implementation could look like :
public class MyDummyAuthenticator implements Authenticator {
#Override
public User authenticate(HttpServletRequest httpServletRequest) {
return new User("mytestuser#domain.com");
}
}
You can of course make it more complicated. Since you have access to the HttpServletRequest you can get the user's email from a HTTP header or something like it.
Note that with an Authenticator you have access to the session in the local server but not in production. In production, httpServletRequest.getSession() will return null. THere's a trick to still fetch the session from the datastore, which I explain here.
Then there's the question of how to keep both the normal authentication solution and your DummyAuthenticator implementation. I think you can chain authenticators, but I'm not sure how it works. In the worst case, you can just swap the Authenticator implementation during your flights.

EmberJS Rails API security

Setup is an Ember frontend with a rails backend using JSON api.
Everything is going fine but some questions do come up:
How do I ensure only the emberjs application consumes the api? I wouldn't want a scripter to write an application to consume the backend api.
It all seems pretty insecure because the EmberJS application would come in a .js file to the client.
How would I ensure a user is really that user if everyone has access to a JS console?
You can extend the RESTAdapter and override the ajax method to include your authentication token in the hash, and you need make sure your controllers validate that token.
In my environment (.NET), I have the authentication token in a hidden field of the document which my app renders, so my ajax override looks like this:
App.Adapter = DS.RESTAdapter.extend({
ajax: function(url, type, hash, dataType) {
hash.url = url;
hash.type = type;
hash.dataType = dataType || 'json';
hash.contentType = 'application/json; charset=utf-8';
hash.context = this;
if (hash.data && type !== 'GET') {
hash.data = JSON.stringify(hash.data);
}
var antiForgeryToken = $('#antiForgeryTokenHidden').val();
if (antiForgeryToken) {
hash = {
'RequestVerificationToken': antiForgeryToken
};
}
jQuery.ajax(hash);
}
});
The token can come from a cookie or whatever you define, as long as you're able to include it in the request header and have your controllers validate it (possibly in before_filter), it should enough.
Then in the Store, pass the new adapter instead of the default (which is RESTAdapter)
App.Store = DS.Store.extend({
revision: 12,
adapter: App.Adapter.create()
})
Note: RESTAdapter#ajax will be changed in favor or Ember.RSVP, making this override deprecated. It must be updated after the next release, but should be ok for revision 12.
I am using Ember Simple Auth to great effect for user authentication and API authorisation.
I use the Oauth 2 user password grant type for authentication of the user and authorising the application by way of a bearer token which must be sent on all future API requests. This means the user enters their username/email and password into the client app which then sends to the server via HTTPS to get an authorisation token and possibly a refresh token. All requests must be over HTTPS to protect disclosure of the bearer token.
I have this in app/initializers/auth:
Em.Application.initializer
name: 'authentication'
initialize: (container, application) ->
Em.SimpleAuth.Authenticators.OAuth2.reopen
serverTokenEndpoint: 'yourserver.com/api/tokens'
Em.SimpleAuth.setup container, application,
authorizerFactory: 'authorizer:oauth2-bearer'
crossOriginWhitelist: ['yourserver.com']
In app/controllers/login.coffee:
App.LoginController = Em.Controller.extend Em.SimpleAuth.LoginControllerMixin,
authenticatorFactory: 'ember-simple-auth-authenticator:oauth2-password-grant'
In app/routes/router.coffee:
App.Router.map ->
#route 'login'
# other routes as required...
In app/routes/application.coffee:
App.ApplicationRoute = App.Route.extend Em.SimpleAuth.ApplicationRouteMixin
In app/routes/protected.coffee:
App.ProtectedRoute = Ember.Route.extend Em.SimpleAuth.AuthenticatedRouteMixin
In templates/login.hbs (I am using Ember EasyForm):
{{#form-for controller}}
{{input identification
label="User"
placeholder="you#example.com"
hint='Enter your email address.'}}
{{input password
as="password"
hint="Enter your password."
value=password}}
<button type="submit" {{action 'authenticate' target=controller}}>Login</button>
{{/form-for}}
To protect a route I just extend from App.ProtectedRoute or use the protected route mixin.
Your server will need to handle the Oauth 2 request and response at the configured server token endpoint above. This is very easy to do, Section 4.3 of RFC 6749 describes the request and response if your server side framework doesn't have built-in support for Oauth2. You will need to store, track and expire these tokens on your server however. There are approaches to avoiding storage of tokens but that's beyond the scope of the question :)
I have answered the backend question and provided example rails example code for user authentication, API authorisation and token authentication here

HTTP headers in Websockets client API

Looks like it's easy to add custom HTTP headers to your websocket client with any HTTP header client which supports this, but I can't find how to do it with the web platform's WebSocket API.
Anyone has a clue on how to achieve it?
var ws = new WebSocket("ws://example.com/service");
Specifically, I need to be able to send an HTTP Authorization header.
Updated 2x
Short answer: No, only the path and protocol field can be specified.
Longer answer:
There is no method in the JavaScript WebSockets API for specifying additional headers for the client/browser to send. The HTTP path ("GET /xyz") and protocol header ("Sec-WebSocket-Protocol") can be specified in the WebSocket constructor.
The Sec-WebSocket-Protocol header (which is sometimes extended to be used in websocket specific authentication) is generated from the optional second argument to the WebSocket constructor:
var ws = new WebSocket("ws://example.com/path", "protocol");
var ws = new WebSocket("ws://example.com/path", ["protocol1", "protocol2"]);
The above results in the following headers:
Sec-WebSocket-Protocol: protocol
and
Sec-WebSocket-Protocol: protocol1, protocol2
A common pattern for achieving WebSocket authentication/authorization is to implement a ticketing system where the page hosting the WebSocket client requests a ticket from the server and then passes this ticket during WebSocket connection setup either in the URL/query string, in the protocol field, or required as the first message after the connection is established. The server then only allows the connection to continue if the ticket is valid (exists, has not been already used, client IP encoded in ticket matches, timestamp in ticket is recent, etc). Here is a summary of WebSocket security information: https://devcenter.heroku.com/articles/websocket-security
Basic authentication was formerly an option but this has been deprecated and modern browsers don't send the header even if it is specified.
Basic Auth Info (Deprecated - No longer functional):
NOTE: the following information is no longer accurate in any modern browsers.
The Authorization header is generated from the username and password (or just username) field of the WebSocket URI:
var ws = new WebSocket("ws://username:password#example.com")
The above results in the following header with the string "username:password" base64 encoded:
Authorization: Basic dXNlcm5hbWU6cGFzc3dvcmQ=
I have tested basic auth in Chrome 55 and Firefox 50 and verified that the basic auth info is indeed negotiated with the server (this may not work in Safari).
Thanks to Dmitry Frank's for the basic auth answer
More of an alternate solution, but all modern browsers send the domain cookies along with the connection, so using:
var authToken = 'R3YKZFKBVi';
document.cookie = 'X-Authorization=' + authToken + '; path=/';
var ws = new WebSocket(
'wss://localhost:9000/wss/'
);
End up with the request connection headers:
Cookie: X-Authorization=R3YKZFKBVi
Sending Authorization header is not possible.
Attaching a token query parameter is an option. However, in some circumstances, it may be undesirable to send your main login token in plain text as a query parameter because it is more opaque than using a header and will end up being logged whoknowswhere. If this raises security concerns for you, an alternative is to use a secondary JWT token just for the web socket stuff.
Create a REST endpoint for generating this JWT, which can of course only be accessed by users authenticated with your primary login token (transmitted via header). The web socket JWT can be configured differently than your login token, e.g. with a shorter timeout, so it's safer to send around as query param of your upgrade request.
Create a separate JwtAuthHandler for the same route you register the SockJS eventbusHandler on. Make sure your auth handler is registered first, so you can check the web socket token against your database (the JWT should be somehow linked to your user in the backend).
HTTP Authorization header problem can be addressed with the following:
var ws = new WebSocket("ws://username:password#example.com/service");
Then, a proper Basic Authorization HTTP header will be set with the provided username and password. If you need Basic Authorization, then you're all set.
I want to use Bearer however, and I resorted to the following trick: I connect to the server as follows:
var ws = new WebSocket("ws://my_token#example.com/service");
And when my code at the server side receives Basic Authorization header with non-empty username and empty password, then it interprets the username as a token.
You cannot add headers but, if you just need to pass values to the server at the moment of the connection, you can specify a query string part on the url:
var ws = new WebSocket("ws://example.com/service?key1=value1&key2=value2");
That URL is valid but - of course - you'll need to modify your server code to parse it.
You can not send custom header when you want to establish WebSockets connection using JavaScript WebSockets API.
You can use Subprotocols headers by using the second WebSocket class constructor:
var ws = new WebSocket("ws://example.com/service", "soap");
and then you can get the Subprotocols headers using Sec-WebSocket-Protocol key on the server.
There is also a limitation, your Subprotocols headers values can not contain a comma (,) !
For those still struggling in 2021, Node JS global web sockets class has an additional options field in the constructor. if you go to the implementation of the the WebSockets class, you will find this variable declaration. You can see it accepts three params url, which is required, protocols(optional), which is either a string, an array of strings or null. Then a third param which is options. our interest, an object and (still optional). see ...
declare var WebSocket: {
prototype: WebSocket;
new (
uri: string,
protocols?: string | string[] | null,
options?: {
headers: { [headerName: string]: string };
[optionName: string]: any;
} | null,
): WebSocket;
readonly CLOSED: number;
readonly CLOSING: number;
readonly CONNECTING: number;
readonly OPEN: number;
};
If you are using a Node Js library like react , react-native. here is an example of how you can do it.
const ws = new WebSocket(WEB_SOCKETS_URL, null, {
headers: {
['Set-Cookie']: cookie,
},
});
Notice for the protocols I have passed null. If you are using jwt, you can pass the Authorisation header with Bearer + token
Disclaimer, this might not be supported by all browsers outside the box, from the MDN web docs you can see only two params are documented.
see https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/WebSocket#syntax
Totally hacked it like this, thanks to kanaka's answer.
Client:
var ws = new WebSocket(
'ws://localhost:8080/connect/' + this.state.room.id,
store('token') || cookie('token')
);
Server (using Koa2 in this example, but should be similar wherever):
var url = ctx.websocket.upgradeReq.url; // can use to get url/query params
var authToken = ctx.websocket.upgradeReq.headers['sec-websocket-protocol'];
// Can then decode the auth token and do any session/user stuff...
In my situation (Azure Time Series Insights wss://)
Using the ReconnectingWebsocket wrapper and was able to achieve adding headers with a simple solution:
socket.onopen = function(e) {
socket.send(payload);
};
Where payload in this case is:
{
"headers": {
"Authorization": "Bearer TOKEN",
"x-ms-client-request-id": "CLIENT_ID"
},
"content": {
"searchSpan": {
"from": "UTCDATETIME",
"to": "UTCDATETIME"
},
"top": {
"sort": [
{
"input": {"builtInProperty": "$ts"},
"order": "Asc"
}],
"count": 1000
}}}
to all future debugger - until today i.e 15-07-21
Browser also don't support sending customer headers to the server, so any such code
import * as sock from 'websocket'
const headers = {
Authorization: "bearer " + token
};
console.log(headers);
const wsclient = new sock.w3cwebsocket(
'wss://' + 'myserver.com' + '/api/ws',
'',
'',
headers,
null
);
This is not going to work in browser. The reason behind that is browser native Websocket constructor does not accept headers.
You can easily get misguided because w3cwebsocket contractor accepts headers as i have shown above. This works in node.js however.
The recommended way to do this is through URL query parameters
// authorization: Basic abc123
// content-type: application/json
let ws = new WebSocket(
"ws://example.com/service?authorization=basic%20abc123&content-type=application%2Fjson"
);
This is considered a safe best-practice because:
Headers aren't supported by WebSockets
Headers are advised against during the HTTP -> WebSocket upgrade because CORS is not enforced
SSL encrypts query paramaters
Browsers don't cache WebSocket connections the same way they do with URLs
What I have found works best is to send your jwt to the server just like a regular message. Have the server listening for this message and verify at that point. If valid add it to your stored list of connections. Otherwise send back a message saying it was invalid and close the connection. Here is the client side code. For context the backend is a nestjs server using Websockets.
socket.send(
JSON.stringify({
event: 'auth',
data: jwt
})
);
My case:
I want to connect to a production WS server a www.mycompany.com/api/ws...
using real credentials (a session cookie)...
from a local page (localhost:8000).
Setting document.cookie = "sessionid=foobar;path=/" won't help as domains don't match.
The solution:
Add 127.0.0.1 wsdev.company.com to /etc/hosts.
This way your browser will use cookies from mycompany.com when connecting to www.mycompany.com/api/ws as you are connecting from a valid subdomain wsdev.company.com.
You can pass the headers as a key-value in the third parameter (options) inside an object.
Example with Authorization token. Left the protocol (second parameter) as null
ws = new WebSocket(‘ws://localhost’, null, { headers: { Authorization: token }})
Edit: Seems that this approach only works with nodejs library not with standard browser implementation. Leaving it because it might be useful to some people.
Technically, you will be sending these headers through the connect function before the protocol upgrade phase. This worked for me in a nodejs project:
var WebSocketClient = require('websocket').client;
var ws = new WebSocketClient();
ws.connect(url, '', headers);

Categories

Resources