Lighthouse Graphql Subscriptions authentication returns 403 (subscriberByChannel() returns empty) - pusher

I'm trying to authenticate Subscriptions with Lighthouse (docs)
const pusher = new Pusher('blabla', {
cluster: 'eu',
forceTLS: true,
authEndpoint: 'https://app.test/graphql/subscriptions/auth',
auth: {
headers: {
Authorization: `Bearer blabla`
}
}
})
But when I run a subscription query, I get this
Unable to retrieve auth string from auth endpoint.
Received status 403 from https://app.test/graphql/subscriptions/auth.
Clients must be authenticated to join private or presence channels.
See: https://pusher.com/docs/authenticating_users
So I decided to follow the flow.
And here is where it fails:
//
// Nuwave\Lighthouse\Subscriptions > class Authorizer
//
public function authorize(Request $request): bool
$subscriber = $this->storage->subscriberByRequest(
$request->input(),
$request->headers->all()
);
// 🔴 "$subscriber" is empty here. ==> return false
if (! $subscriber) {
return false;
}
...
}
By stepping inside $this->storage->subscriberByRequest(), I get
//
// Nuwave\Lighthouse\Subscriptions > class StorageManager
//
public function subscriberByRequest(array $input, array $headers): ?Subscriber
{
$channel = Arr::get($input, 'channel_name');
return $channel
? $this->subscriberByChannel($channel)
: null;
}
public function subscriberByChannel(string $channel): ?Subscriber
{
$key = self::SUBSCRIBER_KEY.".{$channel}";
// At this point, "$key" equals "graphql.subscriber.private-lighthouse-blablabla"
return $this->cache->get($key); // 🔴 This returns NULL
}
Can someone help?
Did I miss something?
My config:
# composer.json
"php": "^7.4",
"laravel/framework": "^7.0",
"nuwave/lighthouse": "^4.11",
"pusher/pusher-php-server": "^4.1"
# .env
BROADCAST_DRIVER=pusher
CACHE_DRIVER=file
LIGHTHOUSE_SUBSCRIPTION_STORAGE=file
LIGHTHOUSE_BROADCASTER=pusher
# config/lighthouse.php
'subscriptions' => [
'storage' => env('LIGHTHOUSE_SUBSCRIPTION_STORAGE', 'file'),
'broadcaster' => env('LIGHTHOUSE_BROADCASTER', 'pusher')
],

Related

Loopback 4 authentication metadata options undefined

I have created a simple jwt auth application the same way its displayed here: https://github.com/raymondfeng/loopback4-example-auth0
The authentication part is working properly but the authorization does not work as expected.
I decorated my controller with following function and added a scope.
#authenticate({strategy: 'auth0-jwt', options: {scopes: ['greet']}})
In my authentication strategy I´m checking the scope via the AuthenticationMetadata class.
import {AuthenticationBindings, AuthenticationMetadata, AuthenticationStrategy} from '#loopback/authentication';
import {inject} from '#loopback/core';
import {ExpressRequestHandler, Request, Response, RestBindings} from '#loopback/rest';
import {UserProfile} from '#loopback/security';
import {JWT_SERVICE} from './types';
const jwtAuthz = require('express-jwt-authz');
export class JWTAuthenticationStrategy implements AuthenticationStrategy {
name = 'auth0-jwt';
constructor(
#inject(RestBindings.Http.RESPONSE)
private response: Response,
#inject(AuthenticationBindings.METADATA)
private metadata: AuthenticationMetadata,
#inject(JWT_SERVICE)
private jwtCheck: ExpressRequestHandler,
) {}
async authenticate(request: Request): Promise<UserProfile | undefined> {
return new Promise<UserProfile | undefined>((resolve, reject) => {
this.jwtCheck(request, this.response, (err: unknown) => {
if (err) {
console.error(err);
reject(err);
return;
}
console.log(this.metadata.options);
// If the `#authenticate` requires `scopes` check
if (this.metadata.options?.scopes) {
jwtAuthz(this.metadata.options!.scopes, {failWithError: true})(request, this.response, (err2?: Error) => {
if (err2) {
console.error(err2);
reject(err2);
return;
}
// eslint-disable-next-line #typescript-eslint/no-explicit-any
resolve((request as any).user);
});
} else {
// eslint-disable-next-line #typescript-eslint/no-explicit-any
resolve((request as any).user);
}
});
});
}
}
When trying to access
this.metadata.options
I´m always getting an undefined back.
How can I achieve to get the options and the scope out of metadata?
Thanks
For Loopback Authorization your class needs to implement the Provider<Authorizer> interface. In that interface it defines the 2 functions you need to implement
#injectable({scope: BindingScope.TRANSIENT})
class AuthorizationService implements Provider<Authorizer>{
value (): Authorizer {
return this.authorize.bind(this);
}
async authorize (
context: AuthorizationContext,
metadata: AuthorizationMetadata,
) {
// TODO implement authorization
}
}
The authorization metadata will be injected by loopback into that function automatically after you bind it with an AuthorizationTags.Authorizer
If you are having problems implementing Authentication then read my step by step guide on how we implemented Loopback Authentication using Firebase. That should be able to help you with the core ideas to get Authentication running.

Service/Microsoft.AspNetCore.Cors.Infrastructure.CorsMiddleware] No CORS policy found for the specified request

This might be an already answered question but I have tried already what I have found on stackoverflow.
This is the problem:
No cors policy found, how to fix this?
Situation: I have an identity server 4 server, that allows external authentication (oidc), 2 external servers so far. One works fine the other one (Azure AD B2C) don't.
This is my startup class:
public class Startup
{
public readonly Microsoft.Extensions.Logging.ILogger<IdentityServer4.Services.DefaultCorsPolicyService> logger;
public void ConfigureServices(IServiceCollection services)
{
services.AddControllersWithViews().AddDapr();
var builder = services.AddIdentityServer(opt => opt.IssuerUri = "http://tenant")
.AddInMemoryIdentityResources(Config.IdentityResources)
.AddInMemoryApiScopes(Config.ApiScopes)
.AddInMemoryApiResources(Config.ApiResources)
.AddInMemoryClients(Config.Clients)
.AddDeveloperSigningCredential();
/*var cors = new IdentityServer4.Services.DefaultCorsPolicyService(logger)
{
AllowAll = true
};*/ //this didn't work
services.AddSingleton<IdentityServer4.Services.ICorsPolicyService>((container) => {
var logger = container.GetRequiredService<Microsoft.Extensions.Logging.ILogger<IdentityServer4.Services.DefaultCorsPolicyService>>();
return new IdentityServer4.Services.DefaultCorsPolicyService(logger)
{
AllowAll = true
};
});
/*services.AddCors(options =>
{
options.AddDefaultPolicy(builder =>
{
builder.AllowAnyOrigin()
.AllowAnyHeader()
.AllowAnyMethod();
});
});*/ //didn't work
/*services.AddCors(options =>
{
options.AddDefaultPolicy(
builder => builder.AllowAnyOrigin().AllowAnyMethod().AllowAnyHeader()
);
});*/
AddServices(services);
services.AddOidcStateDataFormatterCache();
AddAuth(services);
AddSwaggerGeneration(services);
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
app.UseCors();
if (env.IsDevelopment())
{
app.UseExceptionHandler("/error-local-development");
app.UseSwagger();
app.UseSwaggerUI(c => c.SwaggerEndpoint("/swagger/v1/swagger.json", "Tenant v1"));
}
else
{
app.UseExceptionHandler("/error");
}
app.UseStaticFiles();
app.UseRouting();
app.UseIdentityServer();
app.UseAuthorization();
app.UseCloudEvents();
app.UseEndpoints(endpoints =>
{
endpoints.MapDefaultControllerRoute();
});
}
private void AddServices(IServiceCollection services)
{
services.AddSingleton<ITenantRepository, DaprTenantRepository>();
services.AddSingleton<IDaprClientAdaptor, DaprClientAdaptor>();
}
private void AddAuth(IServiceCollection services)
{
services.AddAuthentication()
.AddOpenIdConnect("oidc", "TestIdentityServer", options =>
{
//...code for the first identity provider server, the one that works
})
.AddOpenIdConnect("oidc3", "AzureInternalTestIdentityServer", options =>
{
options.SignInScheme = IdentityServerConstants.ExternalCookieAuthenticationScheme;
options.SignOutScheme = IdentityServerConstants.SignoutScheme;
options.SaveTokens = true;
options.Authority = "https://my azure authority";
options.ClientId = "my clientId";
options.ClientSecret = "secret";
options.ResponseType = "code id_token";
options.Scope.Add("email");
options.GetClaimsFromUserInfoEndpoint = true;
options.TokenValidationParameters = new TokenValidationParameters
{
NameClaimType = "name",
RoleClaimType = "role"
};
});
services.AddAuthorization();
}
private void AddSwaggerGeneration(IServiceCollection services)
{
services.AddVersionedApiExplorer(options => options.GroupNameFormat = "'v'VVV");
services.AddApiVersioning(options =>{
options.DefaultApiVersion = new ApiVersion(1, 0);
options.AssumeDefaultVersionWhenUnspecified = true;
options.ReportApiVersions = true;
});
services.AddSwaggerGen(c =>
{
c.EnableAnnotations();
// Set the comments path for the Swagger JSON and UI.
var xmlFile = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml";
var xmlPath = Path.Combine(AppContext.BaseDirectory, xmlFile);
c.IncludeXmlComments(xmlPath);
c.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme
{
Description = #"JWT Authorization header using the Bearer scheme.
Enter 'Bearer' [space] and then your token in the text input below.
Example: 'Bearer eyJhbGciOiJSU...'",
Name = "Authorization",
In = ParameterLocation.Header,
Type = SecuritySchemeType.ApiKey,
Scheme = "Bearer"
});
c.AddSecurityRequirement(new OpenApiSecurityRequirement()
{
{
new OpenApiSecurityScheme
{
Reference = new OpenApiReference
{
Type = ReferenceType.SecurityScheme,
Id = "Bearer"
},
Scheme = "oauth2",
Name = "Bearer",
In = ParameterLocation.Header,
},
new List<string>()
}
});
});
services.AddTransient<IConfigureOptions<SwaggerGenOptions>, ConfigureSwaggerOptions>();
}
}
As you can see, I am using app.UseCors, I have setup Serilog on program.cs, I don't know if that will impact it.
logs from docker (I am using docker and dapr):
2021-07-13T14:07:39.7018825+00:00 [INF][/Service/Microsoft.AspNetCore.Hosting.Diagnostics] Request starting HTTP/2 GET https://localhost:5007/External/Challenge?scheme=oidc3&returnUrl=%2Fdiagnostics - -
2021-07-13T14:07:39.7093231+00:00 [INF][/Service/Microsoft.AspNetCore.Routing.EndpointMiddleware] Executing endpoint 'IdentityServerHost.Quickstart.UI.ExternalController.Challenge (Tenant)'
2021-07-13T14:07:39.7141041+00:00 [INF][/TenantService/Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker] Route matched with {action = "Challenge", controller = "External"}. Executing controller action with signature Microsoft.AspNetCore.Mvc.IActionResult Challenge(System.String, System.String) on controller IdentityServerHost.Quickstart.UI.ExternalController (Tenant).
2021-07-13T14:07:39.7455289+00:00 [INF][/TenantService/Microsoft.AspNetCore.Mvc.ChallengeResult] Executing ChallengeResult with authentication schemes (["oidc3"]).
2021-07-13T14:07:41.1914398+00:00 [INF][/TenantService/Microsoft.AspNetCore.Authentication.OpenIdConnect.OpenIdConnectHandler] AuthenticationScheme: oidc3 was challenged.
2021-07-13T14:07:41.2103318+00:00 [INF][/TenantService/Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker] Executed action IdentityServerHost.Quickstart.UI.ExternalController.Challenge (Tenant) in 1496.0595ms
2021-07-13T14:07:41.2115812+00:00 [INF][/TenantService/Microsoft.AspNetCore.Routing.EndpointMiddleware] Executed endpoint 'IdentityServerHost.Quickstart.UI.ExternalController.Challenge (Tenant)'
2021-07-13T14:07:41.2454932+00:00 [INF][/TenantService/Microsoft.AspNetCore.Hosting.Diagnostics] Request finished HTTP/2 GET https://localhost:5007/External/Challenge?scheme=oidc3&returnUrl=%2Fdiagnostics - - - 302 0 - 1543.5567ms
2021-07-13T14:07:41.8513003+00:00 [INF][/TenantService/Microsoft.AspNetCore.Hosting.Diagnostics] Request starting HTTP/2 POST https://localhost:5007/signin-oidc application/x-www-form-urlencoded 427
2021-07-13T14:07:41.8578905+00:00 [INF][/TenantService/Microsoft.AspNetCore.Cors.Infrastructure.CorsMiddleware] No CORS policy found for the specified request.
2021-07-13T14:07:41.8630414+00:00 [INF][/TenantService/Microsoft.AspNetCore.Cors.Infrastructure.CorsMiddleware] No CORS policy found for the specified request.
2021-07-13T14:07:42.0976889+00:00 [ERR][/TenantService/Microsoft.AspNetCore.Authentication.OpenIdConnect.OpenIdConnectHandler] Exception occurred while processing message.
System.Security.Cryptography.CryptographicException: The payload was invalid.
at Microsoft.AspNetCore.DataProtection.Managed.ManagedAuthenticatedEncryptor.Decrypt(ArraySegment`1 protectedPayload, ArraySegment`1 additionalAuthenticatedData)
at Microsoft.AspNetCore.DataProtection.KeyManagement.KeyRingBasedDataProtector.UnprotectCore(Byte[] protectedData, Boolean allowOperationsOnRevokedKeys, UnprotectStatus& status)
at Microsoft.AspNetCore.DataProtection.KeyManagement.KeyRingBasedDataProtector.DangerousUnprotect(Byte[] protectedData, Boolean ignoreRevocationErrors, Boolean& requiresMigration, Boolean& wasRevoked)
at Microsoft.AspNetCore.DataProtection.KeyManagement.KeyRingBasedDataProtector.Unprotect(Byte[] protectedData)
at Microsoft.AspNetCore.DataProtection.DataProtectionCommonExtensions.Unprotect(IDataProtector protector, String protectedData)
at IdentityServer4.Infrastructure.DistributedCacheStateDataFormatter.Unprotect(String protectedText, String purpose)
at IdentityServer4.Infrastructure.DistributedCacheStateDataFormatter.Unprotect(String protectedText)
at Microsoft.AspNetCore.Authentication.OpenIdConnect.OpenIdConnectHandler.ReadPropertiesAndClearState(OpenIdConnectMessage message)
at Microsoft.AspNetCore.Authentication.OpenIdConnect.OpenIdConnectHandler.HandleRemoteAuthenticateAsync()
2021-07-13T14:07:42.6735507+00:00 [INF][/TenantService/Microsoft.AspNetCore.Authentication.OpenIdConnect.OpenIdConnectHandler] Error from RemoteAuthentication: The payload was invalid..
2021-07-13T14:07:43.1458887+00:00 [ERR][/TenantService/Microsoft.AspNetCore.Diagnostics.ExceptionHandlerMiddleware] An unhandled exception has occurred while executing the request.
System.Exception: An error was encountered while handling the remote login.
---> System.Security.Cryptography.CryptographicException: The payload was invalid.
at Microsoft.AspNetCore.DataProtection.Managed.ManagedAuthenticatedEncryptor.Decrypt(ArraySegment`1 protectedPayload, ArraySegment`1 additionalAuthenticatedData)
at Microsoft.AspNetCore.DataProtection.KeyManagement.KeyRingBasedDataProtector.UnprotectCore(Byte[] protectedData, Boolean allowOperationsOnRevokedKeys, UnprotectStatus& status)
at Microsoft.AspNetCore.DataProtection.KeyManagement.KeyRingBasedDataProtector.DangerousUnprotect(Byte[] protectedData, Boolean ignoreRevocationErrors, Boolean& requiresMigration, Boolean& wasRevoked)
at Microsoft.AspNetCore.DataProtection.KeyManagement.KeyRingBasedDataProtector.Unprotect(Byte[] protectedData)
at Microsoft.AspNetCore.DataProtection.DataProtectionCommonExtensions.Unprotect(IDataProtector protector, String protectedData)
at IdentityServer4.Infrastructure.DistributedCacheStateDataFormatter.Unprotect(String protectedText, String purpose)
at IdentityServer4.Infrastructure.DistributedCacheStateDataFormatter.Unprotect(String protectedText)
at Microsoft.AspNetCore.Authentication.OpenIdConnect.OpenIdConnectHandler.ReadPropertiesAndClearState(OpenIdConnectMessage message)
at Microsoft.AspNetCore.Authentication.OpenIdConnect.OpenIdConnectHandler.HandleRemoteAuthenticateAsync()
--- End of inner exception stack trace ---
at Microsoft.AspNetCore.Authentication.RemoteAuthenticationHandler`1.HandleRequestAsync()
at IdentityServer4.Hosting.FederatedSignOut.AuthenticationRequestHandlerWrapper.HandleRequestAsync()
at Microsoft.AspNetCore.Authentication.AuthenticationMiddleware.Invoke(HttpContext context)
at IdentityServer4.Hosting.BaseUrlMiddleware.Invoke(HttpContext context)
at Swashbuckle.AspNetCore.SwaggerUI.SwaggerUIMiddleware.Invoke(HttpContext httpContext)
at Swashbuckle.AspNetCore.Swagger.SwaggerMiddleware.Invoke(HttpContext httpContext, ISwaggerProvider swaggerProvider)
at Microsoft.AspNetCore.Diagnostics.ExceptionHandlerMiddleware.<Invoke>g__Awaited|6_0(ExceptionHandlerMiddleware middleware, HttpContext context, Task task)
2021-07-13T14:07:43.2072371+00:00 [INF][/TenantService/Microsoft.AspNetCore.Cors.Infrastructure.CorsMiddleware] No CORS policy found for the specified request.
2021-07-13T14:07:43.2084321+00:00 [INF][/TenantService/Microsoft.AspNetCore.Routing.EndpointMiddleware] Executing endpoint 'TenantsService.V1.Controllers.ErrorController.ErrorLocalDevelopment (Tenant)'
2021-07-13T14:07:43.2205122+00:00 [INF][/TenantService/Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker] Route matched with {action = "ErrorLocalDevelopment", controller = "Error"}. Executing controller action with signature Microsoft.AspNetCore.Mvc.IActionResult ErrorLocalDevelopment(Microsoft.AspNetCore.Hosting.IWebHostEnvironment) on controller TenantsService.V1.Controllers.ErrorController (Tenant).
2021-07-13T14:07:43.2723048+00:00 [INF][/TenantService/Microsoft.AspNetCore.Mvc.Infrastructure.ObjectResultExecutor] Executing ObjectResult, writing value of type 'Microsoft.AspNetCore.Mvc.ProblemDetails'.
2021-07-13T14:07:43.3656934+00:00 [INF][/TenantService/Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker] Executed action TenantsService.V1.Controllers.ErrorController.ErrorLocalDevelopment (Tenant) in 145.0361ms
2021-07-13T14:07:43.3658905+00:00 [INF][/TenantService/Microsoft.AspNetCore.Routing.EndpointMiddleware] Executed endpoint 'TenantsService.V1.Controllers.ErrorController.ErrorLocalDevelopment (Tenant)'
2021-07-13T14:07:43.3662553+00:00 [INF][/TenantService/Microsoft.AspNetCore.Hosting.Diagnostics] Request finished HTTP/2 POST https://localhost:5007/signin-oidc application/x-www-form-urlencoded 427 - 500 - application/problem+json;+charset=utf-8 1514.9476ms
I don't believe CORS is required here as the Oauth2/Open ID Connection mainly bounce around using HTTP redirects. and I am not sure exactly when CORS are required in Identity Server, maybe when you want to retrieve user info with the idtoken using ajax?
The thing with Azure AD B2C is that it gets a few additional parameters like sign in /out policy that I believe the normal OpenID Connect middleware cannot handle it well. I will suggest try to use MSAL library(Web.Identity.Web) to implement it. So rather than the AddOpenIdConnect call, you do
services.AddMicrosoftIdentityWebAppAuthentication(Configuration, "AzureAdB2C");
as shown here.
https://github.com/Azure-Samples/active-directory-aspnetcore-webapp-openidconnect-v2/blob/master/1-WebApp-OIDC/1-5-B2C/Startup.cs
Good luck!

Could not parse the ephemeral key response following protocol

I am trying to create an ephemeral key in my IOS app. I can successfully create a stripe customer that saves in my firebase console and on my stripe dashboard. However, when I try to create the ephemeral key, I am receiving the error in my ios console after trying to view the checkout controller.
'Could not parse the ephemeral key response following protocol STPCustomerEphemeralKeyProvider. Make sure your backend is sending the unmodified JSON of the ephemeral key to your app.
and on my firebase function logs I am seeing,
createEphemeralKey
Request has incorrect Content-Type.
createEphemeralKey
Invalid request, unable to process.
in my index.js file, the code that I am using is
exports.createEphemeralKey = functions.https.onCall(async(data, context) => {
var stripeVersion = data.api_version;
const customerId = data.customer_id;
return stripe.ephemeralKeys.create(
{customer: customerId},
{stripe_version: stripeVersion}
).then((key) => {
return key
}).catch((err) => {
console.log(err)
})
})
Below is how I create my stripe customer.
exports.createStripeCustomer = functions.auth.user().onCreate((user) => {
return stripe.customers.create({
email: user.email,
}).then((customer) => {
return admin.database().ref(`/stripe_customers/${user.uid}/customer_id`).set(customer.id);
});
});
and then myAPIClient looks like.
enum APIError: Error {
case unknown
var localizedDescription: String {
switch self {
case .unknown:
return "Unknown error"
}
}
}
static let sharedClient = MyAPIClient()
var baseURLString: String? = "https://myProject.cloudfunctions.net/"
var baseURL: URL {
if let urlString = self.baseURLString, let url = URL(string: urlString) {
return url
} else {
fatalError()
}
}
func createCustomerKey(withAPIVersion apiVersion: String, completion: #escaping STPJSONResponseCompletionBlock) {
let url = self.baseURL.appendingPathComponent("ephemeral_keys")
Alamofire.request(url, method: .post, parameters: [
"api_version": apiVersion,
])
.validate(statusCode: 200..<300)
.responseJSON { responseJSON in
switch responseJSON.result {
case .success(let json):
completion(json as? [String: AnyObject], nil)
case .failure(let error):
completion(nil, error)
}
}
}
On my checkOutVC, I have
var stripePublishableKey = "pk_test_testProjectKey"
var backendBaseURL: String? = "https://myProject.cloudfunctions.net"
let customerContext = STPCustomerContext(keyProvider: MyAPIClient())
init(price: Int, settings: Settings) {
if let stripePublishableKey = UserDefaults.standard.string(forKey: "StripePublishableKey") {
self.stripePublishableKey = stripePublishableKey
}
if let backendBaseURL = UserDefaults.standard.string(forKey: "StripeBackendBaseURL") {
self.backendBaseURL = backendBaseURL
}
let stripePublishableKey = self.stripePublishableKey
let backendBaseURL = self.backendBaseURL
assert(stripePublishableKey.hasPrefix("pk_"), "You must set your Stripe publishable key at the top of acceptWorker.swift to run this app.")
assert(backendBaseURL != nil, "You must set your backend base url at the top of acceptWorker.swift to run this app.")
Stripe.setDefaultPublishableKey(self.stripePublishableKey)
let config = STPPaymentConfiguration.shared()
config.appleMerchantIdentifier = self.appleMerchantID
config.companyName = self.companyName
config.requiredBillingAddressFields = settings.requiredBillingAddressFields
config.requiredShippingAddressFields = settings.requiredShippingAddressFields
config.shippingType = settings.shippingType
config.additionalPaymentOptions = settings.additionalPaymentOptions
config.cardScanningEnabled = true
self.country = settings.country
self.paymentCurrency = settings.currency
self.theme = settings.theme
MyAPIClient.sharedClient.baseURLString = self.backendBaseURL
let paymentContext = STPPaymentContext(customerContext: customerContext, configuration: config, theme: settings.theme)
self.paymentContext = STPPaymentContext(customerContext: customerContext)
super.init(nibName: nil, bundle: nil)
self.paymentContext.delegate = self
self.paymentContext.hostViewController = self
self.paymentContext.paymentAmount = 5000 // This is in cents, i.e. $50 USD
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
I apologize for the long lines of code but I am really running into a brick wall. Why isnt the backend creating the ephemeralKey for customers?
Two things are jumping out at me:
You’ve written a callable type function (using onCall) but you’re
trying to call it with a normal HTTP request. These functions need to
be called with Firebase’s client library
(https://firebase.google.com/docs/functions/callable#call_the_function).
This stack overflow answer provides some great links about this:
Firebase Cloud Function to delete user.
Your firebase function is parsing stripe_version and customer_id from
data, but your request is only sending api_version. Where in your
code are you sending stripe_version and customer_id?

identity server4, differents behaviors of login locally / azure

Environments : localhost / azure, .netcore 3.1 mvc identityserver4 + mvc api client.
When I run my application locally, the login/logout works fine, there is :
- an identityserver4 mvc .netcore 3.1
- a client mvc api .netcore 3.1
I can login / logout as much as I want, the login always redirects to the identityserver4 login and the login works.
When the same application with the identityserver4 hosted on Azure
The first login correctly redirects to the azure identityserver4, and login works fine.
Then after the logout (cockies seem to be removed), when I try login again, the redirection to the login page doesn't work and there is an "implicit" login and a direct redirection to the homepage of the website.
The client mvc api is configured like this :
{
"ClientId": "IdentityServer.WebApi",
"ClientSecret": "IdentityServer.WebApi",
"AllowedGrantTypes": "GrantTypes.CodeAndClientCredentials",
"RedirectUris": [
"https://localhost:44372/signin-oidc",
"https://localhost:5001/signin-oidc",
"https://192.168.1.7:44372/signin-oidc",
"https://mogui:44372/signin-oidc"
],
"PostLogoutRedirectUris": [
"https://localhost:44372/signout-callback-oidc",
"https://localhost:5001/signout-callback-oidc",
"https://192.168.1.7:44372/signout-callback-oidc",
"https://mogui:44372/signout-callback-oidc"
],
"AllowedScopes": [
"openid",
"profile"
],
"RequireConsent": true,
"RequirePkce": true,
"AllowOfflineAccess": true
},
The identityserver4 locally / on azure have this kind of code on its Startup class :
public void ConfigureServices(IServiceCollection services)
{
try
{
telemetryClient.TrackTrace("============== Startup ConfigureServices ============== ");
// uncomment, if you wan to add an MVC-based UI
services.AddControllersWithViews();
//services.AddMvc();
string connectionString = Configuration.GetConnectionString("IdentityDbContextConnection");
//const string connectionString = #"Data Source=(LocalDb)\MSSQLLocalDB;database=IdentityServer4.Quickstart.EntityFramework-3.0.102;trusted_connection=yes;";
var migrationsAssembly = typeof(Startup).GetTypeInfo().Assembly.GetName().Name;
services.AddDbContext<IdentityServer.Models.IdentityDbContext>(options =>
options.UseSqlServer(connectionString, sql => sql.MigrationsAssembly(migrationsAssembly))
);
services.AddIdentity<ApplicationUser, IdentityRole>(options =>
{
options.SignIn.RequireConfirmedEmail = true;
})
.AddEntityFrameworkStores<IdentityServer.Models.IdentityDbContext>()
.AddDefaultTokenProviders();
services.AddMvc(options =>
{
options.EnableEndpointRouting = false;
})
.SetCompatibilityVersion(CompatibilityVersion.Latest);
var builder = services.AddIdentityServer(options =>
{
options.Events.RaiseErrorEvents = true;
options.Events.RaiseInformationEvents = true;
options.Events.RaiseFailureEvents = true;
options.Events.RaiseSuccessEvents = true;
options.UserInteraction.LoginUrl = "/Account/Login";
options.UserInteraction.LogoutUrl = "/Account/Logout";
options.Authentication = new AuthenticationOptions()
{
CookieLifetime = TimeSpan.FromHours(10), // ID server cookie timeout set to 10 hours
CookieSlidingExpiration = true
};
})
.AddSigningCredential(X509.GetCertificate("B22BBE7C991CEF13F470481A4042D1E091967FCC")) // signing.crt thumbprint
.AddValidationKey(X509.GetCertificate("321ABA505F6FCDDD00AA5EC2BD307F0C9002F9A8")) // validation.crt thumbprint
.AddConfigurationStore(options =>
{
options.ConfigureDbContext = b => b.UseSqlServer(connectionString, sql => sql.MigrationsAssembly(migrationsAssembly));
})
.AddOperationalStore(options =>
{
options.ConfigureDbContext = b => b.UseSqlServer(connectionString, sql => sql.MigrationsAssembly(migrationsAssembly));
options.EnableTokenCleanup = true;
})
.AddAspNetIdentity<ApplicationUser>();
services.AddAuthentication()
.AddGoogle("Google", options =>
{
options.SignInScheme = IdentityServerConstants.ExternalCookieAuthenticationScheme;
options.ClientId = "174637674775-7bgu471gtme25sr5iagq5agq6riottek.apps.googleusercontent.com";
options.ClientSecret = "V_UsR825ZWxCB9i2xrN-u1Kj";
});
services.AddTransient<IEmailSender, IdentityEmailSender>();
services.AddCors(options => options.AddPolicy("AllowAll", p => p.AllowAnyOrigin()
.AllowAnyMethod()
.AllowAnyHeader()));
services.Configure<CookiePolicyOptions>(options =>
{
options.CheckConsentNeeded = context => true;
options.MinimumSameSitePolicy = SameSiteMode.Strict;
});
services.AddScoped<IProfileService, ProfileService>();
telemetryClient.TrackTrace("============== Startup ConfigureServices finish OK ============== ");
}
catch (Exception e)
{
telemetryClient.TrackTrace("Exception general in ConfigureServices");
telemetryClient.TrackException(e);
throw;
}
}
and this :
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
try
{
telemetryClient.TrackTrace("============== Startup Configure ============== ");
InitializeDatabase(app);
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseStaticFiles();
app.UseCors("AllowAll");
app.UseRouting();
app.UseIdentityServer();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapDefaultControllerRoute();
});
app.UseMvcWithDefaultRoute();
app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");
});
telemetryClient.TrackTrace("============== Startup Configure finish OK============== ");
}
catch (Exception e)
{
telemetryClient.TrackTrace("Exception general in Configure");
telemetryClient.TrackException(e);
throw;
}
}
So the problem is with
the identityserver4 localhost the login / logout works find
the idnetityserver4 hosted on azure the login is skipped and go diectly to the homepage (the user is authenticated with previous login).
Sorry to be a little long,
I haven't seen this exact problem on stackoverflow or somewhere else.
Thanx in advance !
There are several things that can go wrong with moving your app to production.
I suspect that if you are redirected back to your Homepage, that the auth cookies are not being removed by your SignOutAsync("Cookies) call.
Check these:
PostLogoutRedirectUris contain your azure domain + "signout-callback-oidc"
Check on what path is your Auth cookies created. If different from "/" - add the default path. I guess in your case it would be somewhat among the lines of:
options.Authentication = new AuthenticationOptions()
{
CookieLifetime = TimeSpan.FromHours(10), // ID server cookie timeout set to 10 hours
CookieSlidingExpiration = true,
Path = "/"
};
You're right Riste,
According to differents posts I've seen, we can do such a thing :
- first of all, we have to put FrontChannelLogoutUri parameter
(it should be an mvc client controller/action called by identityserver4, in our case
should be something like https://localhost:999/Account/FrontChannelLogout) for the client mvc app, generally it is put in Config.cs and add this parameter for the client Mvc (with RedirectUris, PostLogoutRedirectUris, ...)
- on the client mvc, in an account controller (for instance) where is managed the login ,
we can add / modifiy the logout management :
[Authorize]
public async Task<IActionResult> Logout()
{
var client = new HttpClient();
var disco = await client.GetDiscoveryDocumentAsync($"https://{Startup.Configuration["Auth0:Domain"]}");
return Redirect(disco.EndSessionEndpoint);
}
public async Task<IActionResult> FrontChannelLogout(string sid)
{
if (User.Identity.IsAuthenticated)
{
var currentSid = User.FindFirst("sid")?.Value ?? "";
if (string.Equals(currentSid, sid, StringComparison.Ordinal))
{
await HttpContext.SignOutAsync("oidc");
await HttpContext.SignOutAsync("Identity.Application");
await _signInManager.Context.SignOutAsync("_af");
await _signInManager.Context.SignOutAsync("idsrv.session");
await _signInManager.Context.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
}
}
return NoContent();
}
On the identityserver4 side :
In the QuickStart Account Controller, we have to update the BuildLoggedOutViewModelAsync method :
private async Task<LoggedOutViewModel> BuildLoggedOutViewModelAsync(string logoutId)
{
// get context information (client name, post logout redirect URI and iframe for federated signout)
var logout = await _interaction.GetLogoutContextAsync(logoutId);
var client = await _clientStore.FindEnabledClientByIdAsync(logout.ClientIds.First());
if (!string.IsNullOrEmpty(client.FrontChannelLogoutUri))
{
//var pos = GetNthIndex(client.FrontChannelLogoutUri, '/', 3);
//logout.PostLogoutRedirectUri = client.FrontChannelLogoutUri.Substring(0, Math.Min(client.FrontChannelLogoutUri.Length, pos));
// Here TODO =====> get the real PostLogoutRedirectUri, it should be a controller/action url on the client mvc side and put it in **logout.PostLogoutRedirectUri**
}
var vm = new LoggedOutViewModel
{
AutomaticRedirectAfterSignOut = AccountOptions.AutomaticRedirectAfterSignOut,
PostLogoutRedirectUri = logout?.PostLogoutRedirectUri,
ClientName = string.IsNullOrEmpty(logout?.ClientName) ? logout?.ClientId : logout?.ClientName,
SignOutIframeUrl = logout?.SignOutIFrameUrl,
LogoutId = logoutId
};
if (User?.Identity.IsAuthenticated == true)
{
var idp = User.FindFirst(JwtClaimTypes.IdentityProvider)?.Value;
if (idp != null && idp != IdentityServer4.IdentityServerConstants.LocalIdentityProvider)
{
var providerSupportsSignout = await HttpContext.GetSchemeSupportsSignOutAsync(idp);
if (providerSupportsSignout)
{
if (vm.LogoutId == null)
{
// if there's no current logout context, we need to create one
// this captures necessary info from the current logged in user
// before we signout and redirect away to the external IdP for signout
vm.LogoutId = await _interaction.CreateLogoutContextAsync();
}
vm.ExternalAuthenticationScheme = idp;
}
}
}
return vm;
}
====> Apparently _interaction.GetLogoutContextAsync(logoutId) never return a PostLogoutRedirectUri even though it has been set up for the mvc client (in the Config.cs).
====> by filling this parameter logout.PostLogoutRedirectUri on identityServer4 side it'll redirect the logout to the client app.
Here is what I can say, I don't know if the logout redirect to the client app is a "standard" behavior, don't know if it was planned in identityserver4.
Some links :
https://andersonnjen.com/2019/03/22/identityserver4-global-logout/
How to redirect user to client app after logging out from identity server?
Thanx !

How to add IBM API authentication to my code?

Watson Assistant only supports one Webhook url. The issue is I have multiple nodes in Assistant that need to call different "Actions" I have hosted in IBM Cloud Functions. How can I do that? I tried to code and "Action" that uses the NPM Axios to call the other "Actions" but it fails as it lacks authentication.
Can someone tell me how to do it? I need the BASE URL to be athenticated via my IBM Cloud user API to grant access to my namespace.
this is the action I created that acts as a "Dispatch" for webhooks for Watson Assistant.
/**
*
* main() will be run when you invoke this action
*
* #param Cloud Functions actions accept a single parameter, which must be a JSON object.
*
* #return The output of this action, which must be a JSON object.
*
*
* Version: 1.5 beta (API HUB)
* Date: 24/04/2020
*
*/
const axios = require("axios");
// Change to your "BASE_URL". Type in the "Web Action" url without the name of it at the end. (must NOT contains the end '/')
// All "Actions" must be within the same namespace.
const BASE_URL = "https://jp-tok.functions.cloud.ibm.com/api/v1/web/
const POST = "post";
const GET = "get";
const ANY = "any";
const ALL = "all";
/* List all your API methods
1. method - API Name (This is the "Actions" name at the end of the Web Action URL or just the name)
2. attr - Attributes that should be available in the params object
3. rule - Currently supports 2 rules;
a) any - params is valid if "any" of the attributes are present
b) all - params is valid only if all attributes are present
4. httpmethod -Supports "POST" and "GET"
5. contains - Use for validating GET URL parameters
*/
const API_METHODS = [{
method: "Emails", // Change to your API method "Please put the function name located at the end of the url without "/" example "Email"
attr: ["client_email", "department_email"],
rule: ANY,
httpmethod: POST,
contains: null
},
{
method: "testapi", // If Watson needs to "GET" information to a user or athenticate a user
attr: [],
rule: ALL,
httpmethod: GET,
contains: "?ID="
},
]
// Returns true if the "params" is valid for the given API method
function isValidParam(method, params = {}) {
var attributes = [];
attributes = method.attr;
var isAny = method.rule === ANY;
for (let index = 0; index < attributes.length; index++) {
const attr = attributes[index];
if (isAny) {
if (Object.hasOwnProperty.call(params, attr)) {
return true;
}
} else {
if (!Object.hasOwnProperty.call(params, attr)) {
return false;
}
}
}
if (attributes.length === 0 && method.contains) {
return (JSON.stringify(params).indexOf(method.contains) > -1)
}
// if the code reaches this position, inverse of "isAny" should return the actual validation status.
return !isAny;
}
async function main(params) {
var result = [];
// Stop on first failure
// We iterate through all API methods. Because there can be more than one matching API for the given param type
for (let index = 0; index < API_METHODS.length; index++) {
const apiMethod = API_METHODS[index];
const url = BASE_URL + '/' + apiMethod.method;
if (isValidParam(apiMethod, params)) {
let response = apiMethod.httpmethod === POST ?
await axios.post(url, params) :
await axios.get(url + params); // Expects the parameter to be a string in this case like '?id=345343'
console.log(response);
result.push({
sent: true,
url: url,
request: params,
});
}
}
return {
sent: true,
details: result
};
}
// THe part of the code that needs to be copied to call other functions within the namespace.
/* const API_METHODS = [{
method: "Emails", // Change to your API method "Please put the function name located at the end of the url without "/" example "Email"
* attr: ["bamboo_email", "latitude_email", "latest_r_email", "department_email"],
rule: ANY,
httpmethod: POST,
contains: null
},
{
method: "testapi", // If Watson needs to "GET" information to a user or athenticate a user
attr: [],
rule: ALL,
httpmethod: GET,
contains: "?ID="
},
] */
When I run it in Watson Assistant I get the following error:
Webhook call response body exceeded [1050000] byte limit.response code: 200, request duration: 591, workspace_id: 150088ee-4e86-48f5-afd5-5b4e99d171f8, transaction_id: a8fca023d456d1113e05aaf1f59b1b2b, url_called: https://watson-url-fetch.watson-url-fetcher:443/post?url=https%3A%2F%2Fjp-tok.functions.cloud.ibm.com%2Fapi%2Fv1%2Fweb%2F23d61581-fa68-4979-afa2-0216c17c1b29%2FWatson+Assistant%2FAssistant+Dispatch.json (and there is 1 more error in the log)

Resources