How to access rootValue or context for nodedefination - relayjs

We could check rootValue or context to determine request's role permission for graphql request.
How do we check permission for nodeDefination (request thru 'node' ) when using graphql-relay-js ?

You should be able to access your permissions with either context or a value from rootValue, as both are made available to the resolver function:
export const {nodeField, nodeInterface} = nodeDefinitions(
function resolveObjectFromID(globalId, context, {rootValue}) {
const {type, id} = fromGlobalId(globalId);
// Optionally perform auth logic here with either context or rootValue...
// Then proceed with loading as usually; here, for example, using
// DataLoader.
const loader = rootValue.loaders[type];
return (loader && loader.load(id)) || null;
},
function resolveGraphQLTypeFromObject(object) {
return registeredTypes[object.constructor.name] || null;
},
);
Note that the context param was added in graphql v0.5.0; prior to that you could only use rootValue. Also note that you can perform permission checks at any level within in the query, not just at the node level, because context and rootValue both get propagated down all the way.

Related

MSAL.NET OBO refresh token problems

I am trying to implement an OBO flow through to the graph API on a middle-tier API (.NET 5.0) using MSAL.NET. I'm running into two frustrating problems, and I can't find anyone having similar problems, so I think I'm misunderstanding something!
Problem 1: Whenever I call MSAL's GetAccountAsync, it always returns null when there should be an account loaded.
Problem 2: Whenever I call MSAL's AcquireTokenSilent, I always get the error "No refresh token found in the cache." even though I got one.
Here's what I have:
Once the web app authenticates, it passes through the token to a graph auth endpoint on the API:
var authenticationResult = await ClaimHelper.ClientApplication.AcquireTokenByAuthorizationCode(GraphHelpers.BasicGraphScopes, context.Code).ExecuteAsync();
var apiUserSession = await CouncilWiseAPIHelper.APIClient.Graph.AuthoriseUserAsync(authenticationResult.AccessToken);
which seems to work fine, and passes through a JWT to the API auth endpoint. The API implements an MSAL Confidential Client application and uses the SetBeforeAccess/SetAfterAccess token cache methods to save the cache to a database.
_msalClient = ConfidentialClientApplicationBuilder.Create(_graphConfig.ClientId)
.WithAuthority(AadAuthorityAudience.AzureAdMultipleOrgs)
.WithClientSecret(_graphConfig.ClientSecret)
.Build();
SetSerialiser(serialiser);
public void SetSerialiser(MSALTokenCacheSerialiser serialiser)
{
_msalClient.UserTokenCache.SetBeforeAccessAsync(serialiser.BeforeAccessCallbackAsync);
_msalClient.UserTokenCache.SetAfterAccessAsync(serialiser.AfterAccessCallbackAsync);
}
And the serialiser methods look like this:
public async Task BeforeAccessCallbackAsync(TokenCacheNotificationArgs notification)
{
GraphUserTokenCache tokenCache = await _graphUserTokenCacheRepository.GetByUserIdentifier(notification.SuggestedCacheKey);
if (tokenCache == null)
{
tokenCache = await _graphUserTokenCacheRepository.Get(notification.SuggestedCacheKey);
}
if (tokenCache != null)
{
notification.TokenCache.DeserializeMsalV3(tokenCache.Value);
}
}
public async Task AfterAccessCallbackAsync(TokenCacheNotificationArgs notification)
{
if (!notification.HasTokens)
{
// Delete from the cache
await _graphUserTokenCacheRepository.Delete(notification.SuggestedCacheKey);
}
if (!notification.HasStateChanged)
{
return;
}
GraphUserTokenCache tokenCache;
if (notification.SuggestedCacheKey == notification.Account.HomeAccountId.Identifier)
{
tokenCache = await _graphUserTokenCacheRepository.GetByUserIdentifier(notification.SuggestedCacheKey);
}
else
{
tokenCache = await _graphUserTokenCacheRepository.Get(notification.SuggestedCacheKey);
}
if (tokenCache == null)
{
var cache = notification.TokenCache.SerializeMsalV3();
tokenCache = new GraphUserTokenCache
{
Id = notification.SuggestedCacheKey,
AccountIdentifier = notification.Account.HomeAccountId.ToString(),
Value = cache
};
await _graphUserTokenCacheRepository.Add(tokenCache);
}
else
{
await _graphUserTokenCacheRepository.Update(tokenCache.Id, notification.TokenCache.SerializeMsalV3());
}
}
I can see the token BeforeAccess and AfterAccess methods being called, and I can see the caches being created in the database (encryption has been removed while I'm trying to track down this issue). If I inspect the serialised token cache being saved, it NEVER has a refresh token populated, but if I inspect the requests with fiddler I can see a refresh token was indeed provided.
Finally, here is the code for retrieving the access token which is called whenever a graph request is made:
public async Task<AuthenticationResult> GetAccessToken(string accountId, string jwtBearerToken)
{
try
{
IAccount account = null;
if (accountId.IsNotNullOrEmpty())
{
account = await _msalClient.GetAccountAsync(accountId);
}
var scope = _graphConfig.Scopes.Split(' ');
if (account == null)
{
var result = await _msalClient.AcquireTokenOnBehalfOf(scope,
new UserAssertion(jwtBearerToken))
.ExecuteAsync();
return result;
}
else
{
var result = await _msalClient.AcquireTokenSilent(scope, account)
.ExecuteAsync();
return result;
}
}
catch (MsalClientException ex)
{
ex.CwApiLog();
return null;
}
catch(Exception ex)
{
ex.CwApiLog();
return null;
}
}
When it's called with the jwtBearerToken, it will successfully call AcquireTokenOnBehalfOf() and the token is cached and a result returned, but when I come back to retrieve the account via GetAccountAsync() it always returns null even though I can see the token cache was loaded in BeforeAccessCallbackAsync().
Also, even if I call AcquireTokenSilent() immediately after acquiring the obo token with the account it just returned, I will get an exception saying there is no refresh token in the cache.
I am totally lost on what I'm doing wrong here, any help would be greatly appreciated.
I recently ran into the same problem while running a long runing OBO flow, MSAL has recently implemented an interface ILongRunningWebApi for these use cases you can go and see this new documentation
Here is an extract:
One OBO scenario is when a web API runs long running processes on
behalf of the user (for example, OneDrive which creates albums for
you). Starting with MSAL.NET 4.38.0, this can be implemented as such:
Before you start a long running process, call:
string sessionKey = // custom key or null
var authResult = await ((ILongRunningWebApi)confidentialClientApp)
.InitiateLongRunningProcessInWebApi(
scopes,
userToken,
ref sessionKey)
.ExecuteAsync();
userToken is a user token used to call this web API. sessionKey will
be used as a key when caching and retrieving the OBO token. If set to
null, MSAL will set it to the assertion hash of the passed-in user
token. It can also be set by the developer to something that
identifies a specific user session, like the optional sid claim from
the user token (for more information, see Provide optional claims to
your app). If the cache already contains a valid OBO token with this
sessionKey, InitiateLongRunningProcessInWebApi will return it.
Otherwise, the user token will be used to acquire a new OBO token from
AAD, which will then be cached and returned.
In the long-running process, whenever OBO token is needed, call:
var authResult = await ((ILongRunningWebApi)confidentialClientApp)
.AcquireTokenInLongRunningProcess(
scopes,
sessionKey)
.ExecuteAsync();
Pass the sessionKey which is associated with the current user's
session and will be used to retrieve the related OBO token. If the
token is expired, MSAL will use the cached refresh token to acquire a
new OBO access token from AAD and cache it. If no token is found with
this sessionKey, MSAL will throw a MsalClientException. Make sure to
call InitiateLongRunningProcessInWebApi first.
Hope this helps :)

Configure Workbox to use cached response when network response is a 404

This question was originally asked in a tweet.
Is there a way to configure Workbox to respond with a cached response when a network response has an HTTP status of 404?
Yes, you can create your own custom handlerCallback that accomplishes that. Some of the details will vary based on your specific setup (the cache names, the fallback URLs, etc.), as well as whether you want to use a formal Workbox strategy (like networkFirst instead of fetch()), but in general, the following should work:
// Assume that this URL is already cached somewhere, e.g. precached.
const fallbackUrl = '/404-fallback.html';
const notFoundFallbackHandler = async ({event}) => {
const fetchResponse = await fetch(event.request);
if (fetchResponse.status === 404) {
return caches.match(fallbackUrl);
} else {
return fetchResponse;
}
};
// To apply this handler based on a URL pattern:
workbox.routing.registerRoute(
new RegExp('/some/criteria/to/match'),
notFoundFallbackHandler
);
// Or, to apply this handler for all navigation requests, use this:
// const navigationRoute = new workbox.routing.NavigationRoute(notFoundFallbackHandler);
// workbox.routing.registerRoute(navigationRoute);

accessing Twitter API from Google Apps Script

I'm trying to read in a Google sheet my Twitter timeline.
I've copied the following code reported in the GAS documentation about twitter authentication (omitting step 2 since I'm not using the code inside a UI):
function getTwitterService() {
// Create a new service with the given name. The name will be used when
// persisting the authorized token, so ensure it is unique within the
// scope of the property store.
return OAuth1.createService('twitter')
// Set the endpoint URLs.
.setAccessTokenUrl('https://api.twitter.com/oauth/access_token')
.setRequestTokenUrl('https://api.twitter.com/oauth/request_token')
.setAuthorizationUrl('https://api.twitter.com/oauth/authorize')
// Set the consumer key and secret.
.setConsumerKey('mykey')
.setConsumerSecret('mysecret')
// Set the name of the callback function in the script referenced
// above that should be invoked to complete the OAuth flow.
.setCallbackFunction('authCallback')
// Set the property store where authorized tokens should be persisted.
.setPropertyStore(PropertiesService.getUserProperties());
}
function authCallback(request) {
var twitterService = getTwitterService();
var isAuthorized = twitterService.handleCallback(request);
if (isAuthorized) {
return Logger.log('Success! You can close this tab.');
} else {
return Logger.log('Denied. You can close this tab');
}
}
function makeRequest() {
var twitterService = getTwitterService();
var response = twitterService.fetch('https://api.twitter.com/1.1/statuses/user_timeline.json');
Logger.log(response);
}
but I obtain the message error: Service not authorized. (row 292, file "Service", project "OAuth1").
What's wrong?
I needed to add the following line the first time I execute makeRequest:
var authorizationUrl = twitterService.authorize();
Logger.log(authorizationUrl);
Then, open the url read from the log and authorize the app.
After that, all works fine.

Detect if firebase records are deleted by admin or stop delete events [duplicate]

In the example below, is there a way to get the uid of the user who wrote to /messages/{pushId}/original?
exports.makeUppercase = functions.database.ref('/messages/{pushId}/original')
.onWrite(event => {
// Grab the current value of what was written to the Realtime Database.
const original = event.data.val();
console.log('Uppercasing', event.params.pushId, original);
const uppercase = original.toUpperCase();
// You must return a Promise when performing asynchronous tasks inside a Functions such as
// writing to the Firebase Realtime Database.
// Setting an "uppercase" sibling in the Realtime Database returns a Promise.
return event.data.ref.parent.child('uppercase').set(uppercase);
});
UPDATED ANSWER (v1.0.0+):
As noted in #Bery's answer above, version 1.0.0 of the Firebase Functions SDK introduced a new context.auth object which contains the authentication state such as uid. See "New properties for user auth information" for more details.
ORIGINAL ANSWER (pre v1.0.0):
Yes, this is technically possible, although it is not currently documented. The uid is stored with the event.auth object. When a Database Cloud Function is triggered from an admin situation (for example, from the Firebase Console data viewer or from an Admin SDK), the value of event.auth is:
{
"admin": true
}
When a Database Cloud Function is triggered from an unauthenticated reference, the value of event.data is:
{
"admin": false
}
And finally, when a Database Cloud Function is triggered from an authed, but not admin, reference, the format of event.auth is:
{
"admin": false,
"variable": {
"provider": "<PROVIDER>",
"provider_id": "<PROVIDER>",
"user_id": "<UID>",
"token": {
// Decoded auth token claims such as sub, aud, iat, exp, etc.
},
"uid": "<UID>"
}
}
Given the information above, your best bet to get the uid of the user who triggered the event is to do the following:
exports.someFunction = functions.database.ref('/some/path')
.onWrite(event => {
var isAdmin = event.auth.admin;
var uid = event.auth.variable ? event.auth.variable.uid : null;
// ...
});
Just note that in the code above, uid would be null even if isAdmin is true. Your exact code depends on your use case.
WARNING: This is currently undocumented behavior, so I'll give my usual caveat of "undocumented features may be changed at any point in the future without notice and even in non-major releases."
Ever since Firebase functions reached version 1.0, this behavior is no longer undocumented but has sligtly changed. Be sure to read the docs.
Context has been added to cloud functions and you can use it like this
exports.dbWrite = functions.database.ref('/path/with/{id}').onWrite((data, context) => {
const authVar = context.auth; // Auth information for the user.
const authType = context.authType; // Permissions level for the user.
const pathId = context.params.id; // The ID in the Path.
const eventId = context.eventId; // A unique event ID.
const timestamp = context.timestamp; // The timestamp at which the event happened.
const eventType = context.eventType; // The type of the event that triggered this function.
const resource = context.resource; // The resource which triggered the event.
// ...
});

User is always null when using AspNet.Security.OpenIdConnect.Server

I'm trying to generate access tokens for my aspnet core web app. I created the following provider:
public class CustomOpenIdConnectServerProvider : OpenIdConnectServerProvider
{
public override Task ValidateTokenRequest(ValidateTokenRequestContext context)
{
// Reject the token requests that don't use grant_type=password or grant_type=refresh_token.
if (!context.Request.IsPasswordGrantType() && !context.Request.IsRefreshTokenGrantType())
{
context.Reject(
error: OpenIdConnectConstants.Errors.UnsupportedGrantType,
description: "Only the resource owner password credentials and refresh token " +
"grants are accepted by this authorization server");
return Task.FromResult(0);
}
// Since there's only one application and since it's a public client
// (i.e a client that cannot keep its credentials private), call Skip()
// to inform the server the request should be accepted without
// enforcing client authentication.
context.Skip();
return Task.FromResult(0);
}
public override async Task HandleTokenRequest(HandleTokenRequestContext context)
{
// Resolve ASP.NET Core Identity's user manager from the DI container.
var manager = context.HttpContext.RequestServices.GetRequiredService<UserManager<User>>();
// Only handle grant_type=password requests and let ASOS
// process grant_type=refresh_token requests automatically.
if (context.Request.IsPasswordGrantType())
{
var user = await manager.FindByNameAsync(context.Request.Username);
if (user == null)
{
context.Reject(
error: OpenIdConnectConstants.Errors.InvalidGrant,
description: "Invalid credentials.");
return;
}
// Ensure the password is valid.
if (!await manager.CheckPasswordAsync(user, context.Request.Password))
{
if (manager.SupportsUserLockout)
{
await manager.AccessFailedAsync(user);
}
context.Reject(
error: OpenIdConnectConstants.Errors.InvalidGrant,
description: "Invalid credentials.");
return;
}
if (manager.SupportsUserLockout)
{
await manager.ResetAccessFailedCountAsync(user);
}
var identity = new ClaimsIdentity(context.Options.AuthenticationScheme);
// Note: the name identifier is always included in both identity and
// access tokens, even if an explicit destination is not specified.
identity.AddClaim(ClaimTypes.NameIdentifier, await manager.GetUserIdAsync(user));
identity.AddClaim(OpenIdConnectConstants.Claims.Subject, await manager.GetUserIdAsync(user));
// When adding custom claims, you MUST specify one or more destinations.
// Read "part 7" for more information about custom claims and scopes.
identity.AddClaim("username", await manager.GetUserNameAsync(user),
OpenIdConnectConstants.Destinations.AccessToken,
OpenIdConnectConstants.Destinations.IdentityToken);
var claims = await manager.GetClaimsAsync(user);
foreach (var claim in claims)
{
identity.AddClaim(claim.Type, claim.Value, OpenIdConnectConstants.Destinations.AccessToken,
OpenIdConnectConstants.Destinations.IdentityToken);
}
// Create a new authentication ticket holding the user identity.
var ticket = new AuthenticationTicket(
new ClaimsPrincipal(identity),
new AuthenticationProperties(),
context.Options.AuthenticationScheme);
// Set the list of scopes granted to the client application.
ticket.SetScopes(
/* openid: */ OpenIdConnectConstants.Scopes.OpenId,
OpenIdConnectConstants.Scopes.OfflineAccess,
/* email: */ OpenIdConnectConstants.Scopes.Email,
/* profile: */ OpenIdConnectConstants.Scopes.Profile);
// Set the resource servers the access token should be issued for.
ticket.SetResources("resource_server");
context.Validate(ticket);
}
}
This works just fine, I can get the access token and the users are authenticated successfully. The issue that I'm facing here is that in any authorized action method when I do this: var user = await _userManager.GetUserAsync(User); the value for user is always null! Of course, I'm passing the Authorization header with a valid access token and the request goes into actions annotated with Authorize without any problems. It's just the value of user is null. Can anybody tell me whats wrong with my code?
By default, UserManager.GetUserAsync(User) uses the ClaimTypes.NameIdentifier claim as the user identifier.
In your case, ClaimTypes.NameIdentifier - which is no longer considered by the OpenID Connect server middleware as a special claim in 1.0 - is not added to the access token because it doesn't have the appropriate destination. As a consequence, Identity is unable to extract the user identifier from the access token.
You have 3 options to fix that:
Replace the default user identifier claim used by Identity by calling services.Configure<IdentityOptions>(options => options.ClaimsIdentity.UserIdClaimType = OpenIdConnectConstants.Claims.Subject); in your Startup.ConfigureServices() method.
Keep using the ClaimTypes.NameIdentifier claim but give it the right destination (OpenIdConnectConstants.Destinations.AccessToken).
Use UserManager.FindByIdAsync(User.FindFirstValue(OpenIdConnectConstants.Claims.Subject)) instead of UserManager.GetUserAsync(User).

Resources