I have deployed Ory Kratos, Ory Oathkeeper, Starburst and Apache Ranger. The goal is to allow users to create their accounts with Ory Kratos, then protect the Starburst UI with Ory Oathkeeper so that once the user is authenticated with Ory Kratos and hits the https://proxy.oathkeeper.mydomain.com/starburst/ we would send the user to the Starburst UI with a JWT in hand that can be verified against the jwks.json file that we have created with Ory Oathkeeper. Finally using the identity of the currently logged in user apply data access policies with Apache Ranger.
The Kratos UI is on https://kratos.mydomain.com
Kratos public is on https://public.kratos.mydomain.com
Kratos admin is on https://admin.kratos.mydomain.com
The jwks.json is on https://auth.mydomain.com/assets/well-known/jwks.json
The Ory Oathkeeper proxy is on https://proxy.oathkeeper.mydomain.com
The Ory Oathkeeper api is on https://api.oathkeeper.mydomain.com
Ory Kratos has the following configuration:
(Terraform code to be yaml encoded to be used in the values file in a helm chart)
config = {
version = "v0.10.1"
dsn = "postgres://USERNAME:PASSWORD#${local.postgres_host}/kratos?sslmode=disable"
courier = {
smtp = {
connection_uri = "SOME_URI"
}
}
cookies = {
domain = "mydomain.com"
path = "/"
same_site = "Lax"
}
session = {
lifespan = "2h"
cookie = {
name = "ory_kratos_session"
persistent = true
path = "/"
same_site = "Lax"
domain = "mydomain.com"
}
}
selfservice = {
default_browser_return_url = "https://${local.kratos_ui_dns}/"
methods = {
password = {
enabled = true
}
}
flows = {
login = {
ui_url = "https://${local.kratos_ui_dns}/login"
lifespan = "10m"
}
error = {
ui_url = "https://${local.kratos_ui_dns}/error"
}
settings = {
ui_url = "https://${local.kratos_ui_dns}/setting"
privileged_session_max_age = "15m"
}
recovery = {
ui_url = "https://${local.kratos_ui_dns}/recovery"
enabled = true
}
verification = {
ui_url = "https://${local.kratos_ui_dns}/verification"
enabled = true
after = {
default_browser_return_url = "https://${local.kratos_ui_dns}/"
}
}
logout = {
after = {
default_browser_return_url = "https://${local.kratos_ui_dns}/login"
}
}
registration = {
lifespan = "10m"
ui_url = "https://${local.kratos_ui_dns}/registration"
after = {
password = {
hooks = [{ "hook" : "session" }]
}
}
}
}
}
log = {
level = "debug"
format = "text"
}
secrets = {
cookie = ["SOME-COOKIE-INSECURE-VALUE"]
cipher = ["32-LONG-SECRET-NOT-SECURE-AT-ALL"]
}
ciphers = {
algorithm = "noop"
}
hashers = {
algorithm = "bcrypt"
bcrypt = {
cost = 4
}
}
serve = {
public = {
base_url = "http://${local.kratos_public_dns}"
host = ""
port = 80
request_log = {
disable_for_health = true
}
}
admin = {
base_url = "https://kratos-admin.dev.svc.cluster.local"
host = ""
port = 4434
request_log = {
disable_for_health = true
}
}
}
}
This results in a good csrf cookie and a ory_kratos_session cookie in the browser after logging in with Kratos. But no JWT token.
The Ory Oathkeeper configuration looks as follows:
(Terraform code to be yaml encoded to be used in the values file in a helm chart)
config = {
authenticators = {
anonymous = {
enabled = true
}
noop = {
enabled = true
}
cookie_session = {
enabled = true
config = {
check_session_url = "https://public.kratos.mydomain.com/sessions/whoami"
preserve_path = true
extra_from : "#this"
subject_from : "identity.id"
only = [
"ory_kratos_session"
]
}
}
jwt = {
enabled = true
config = {
jwks_urls = [
"https://auth.mydomain.com/assets/well-known/jwks.json"
]
scope_strategy = "none"
target_audience = [
"https://proxy.oathkeeper.mydomain.com/starburst/",
"starburst-ui"
]
trusted_issuers = [
"https://proxy.oathkeeper.mydomain.com"
]
}
}
oauth2_client_credentials = {
enabled = false
config = {
token_url = "somesite/oath2/token"
}
}
}
authorizers = {
allow = {
enabled = true
}
}
mutators = {
noop = {
enabled = true
}
id_token = {
enabled = true
config = {
issuer_url = "https://proxy.oathkeeper.mydomain.com"
jwks_url = "https://auth.mydomain.com/assets/well-known/jwks.json"
claims = jsonencode({
aud = [
"https://proxy.oathkeeper.mydomain.com/starburst",
"starburst-ui"
],
claims = {
sub = "{{print .Subject}}"
}
})
}
}
}
serve = {
proxy = {
port = 4455
cors = {
enabled = false
}
}
api = {
port = 4456
cors = {
enabled = false
}
}
}
}
The access rule looks as follows:
(json)
[
{
"id": "starburst-rule.app",
"upstream": {
"url": "http://starburst.app:8080/",
"preserve_host": true,
"strip_path": "/starburst"
},
"match": {
"url": "https://proxy.oathkeeper.mydomain.com/starburst/<.*>",
"methods": [
"GET",
"POST",
"PUT",
"DELETE",
"PATCH"
]
},
"authenticators": [
{
"handler": "jwt",
"config": {
"jwks_urls": [
"https://auth.mydomain.com/assets/well-known/jwks.json"
],
"scope_strategy": "none",
"target_audience": [
"https://proxy.oathkeeper.mydomain.com/starburst/",
"starburst-ui"
]
}
}
],
"authorizer": {
"handler": "allow"
},
"mutators": [
{
"handler": "id_token"
}
],
"errors": {
"fallback": [
"json"
],
"handlers": {
"redirect": {
"enabled": true,
"config": {
"to": "https://kratos.mydomain.com/login",
"when": [
{
"error": [
"unauthorized",
"forbidden"
],
"request": {
"header": {
"accept": [
"text/html"
]
}
}
}
]
}
},
"json": {
"enabled": true,
"config": {
"verbose": true
}
}
}
}
}
]
The Starburst/Trino configuration looks as follows:
config.properties: |
coordinator=true
node-scheduler.include-coordinator=false
http-server.http.port=8080
http-server.authentication.type=JWT
http-server.authentication.jwt.key-file=https://auth.mydomain.com/assets/well-known/jwks.json
http-server.authentication.jwt.required-issuer=https://proxy.oathkeeper.mydomain.com
http-server.authentication.jwt.required-audience=https://proxy.oathkeeper.mydomain.com/starburst/
discovery.uri=http://localhost:8080
usage-metrics.gathering.initial-delay=1m
usage-metrics.gathering.interval=10m
usage-metrics.cluster-usage-resource.enabled=true
The ory_kratos_session cookie looks like this: (json)
{
"id": "1df8ddb5-9628-4c05-97f5-f32b42119748",
"active": true,
"expires_at": "2022-11-23T15:42:25.323749Z",
"authenticated_at": "2022-11-23T13:42:25.323749Z",
"authenticator_assurance_level": "aal1",
"authentication_methods": [
{
"method": "password",
"aal": "aal1",
"completed_at": "2022-11-23T13:42:25.323746742Z"
}
],
"issued_at": "2022-11-23T13:42:25.323749Z",
"identity": {
"id": "844bbe95-5e16-4e8d-abe4-0f19578e17a4",
"schema_id": "default",
"schema_url": "http://public.kratos.mydomain.com/schemas/ZGVmYXVsdA",
"state": "active",
"state_changed_at": "2022-11-23T11:33:55.700664Z",
"traits": {
"email": "someuser#global.com"
},
"metadata_public": null,
"created_at": "2022-11-23T11:33:55.702463Z",
"updated_at": "2022-11-23T11:33:55.702463Z"
}
}
I do get a 404 response code when i try to access https://proxy.oathkeeper.mydomain.com/foobar as expected.
The problem is that i am getting a 401 when i try to access https://proxy.oathkeeper.mydomain.com/starburst/
With the following error message excerpt from the Ory Oathkeeper Pod:
time=2022-11-23T15:29:45Z level=warning msg=Access request denied audience=application error=map[debug: message:Access credentials are invalid reason: status:Unauthorized status_code:401] granted=false http_host=proxy.oathkeeper.mydomain.com http_method=GET http_url=https://proxy.oathkeeper.mydomain.com/starburst/ http_user_agent=Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/107.0.0.0 Safari/537.36 service_name=ORY Oathkeeper service_version=v0.38.19-beta.1
time=2022-11-23T15:29:45Z level=error msg=An error occurred while handling a request code=401 debug= details=map[] error=The request could not be authorized reason= request-id= status=401 writer=JSON
time=2022-11-23T15:29:45Z level=info msg=completed handling request http_request=map[headers:map[accept:text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9 accept-encoding:gzip, deflate, br accept-language:en,sv;q=0.9 cache-control:no-cache cookie:Value is sensitive and has been redacted. To see the value set config key "log.leak_sensitive_values = true" or environment variable "LOG_LEAK_SENSITIVE_VALUES=true". user-agent:Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/107.0.0.0 Safari/537.36 x-forwarded-for:10.7.223.164 x-forwarded-proto:https] host:proxy.oathkeeper.mydomain.com method:GET path:/starburst/ query:<nil> remote:172.29.44.94:8063 scheme:http] http_response=map[status:401 text_status:Unauthorized took:433.335µs]
And no trace of a Authorization: Bearer <JWT...> or related logs in Starburst.
I am not sure if i have missed to configure something of if the configuraion is wrong and was hoping someone could review this and point me in the right direction.
Ok. I figured it out. Ory Kratos should be configured with Cookie Session, it doesn't provide JWT tokens. Ory Oathkeeper can translate the ory_kratos_session cookie to a jwt token, but it has to be configured for it.
Oathkeeper should have cookie authenticator enabled, noop authorizer (in my case) and id_token authenticator with an valid issuer url, jwks_url, ttl and the audience in the claims configured. As for the Oathkeeper rules, the cookie_session should be used, "allow" authorizer and the mutator of type id_token with the claims in its configuration, my mutator rule configuration contains the audience only, since the rest comes from the main configuration file.
Related
Im trying to attach some user data for display purposes to my identity token in duende identityserver but only succeed in attaching it to my access token.
I have setup my identity server with aspnet identity as the provider.
By default my retrieved identity token and access token are returned as follows:
(id token)
{
"iss": "https://localhost:5001",
"nbf": 1676383755,
"iat": 1676383755,
"exp": 1676384055,
"aud": "frontend",
"amr": [
"pwd"
],
"at_hash": "3TXV8nYmX5Ukn-bmuLPLvQ",
"sid": "B4FA078C6687CA5F7B1BE31615517880",
"sub": "c62bd642-3096-43aa-833f-966103dd3071",
"auth_time": 1676383754,
"idp": "local"
}
(access token)
{
"iss": "https://localhost:5001",
"nbf": 1676383755,
"iat": 1676383755,
"exp": 1676387355,
"scope": [
"openid",
"profile"
],
"amr": [
"pwd"
],
"client_id": "frontend",
"sub": "c62bd642-3096-43aa-833f-966103dd3071",
"auth_time": 1676383754,
"idp": "local",
"sid": "B4FA078C6687CA5F7B1BE31615517880",
"jti": "F9FC411122610B702E27039A7D046698"
}
So no userinfo yet so i decided to implement the IProfileService in my identity server solution:
public class ProfileService : IProfileService
{
private readonly IUserClaimsPrincipalFactory<IdentityUser> _userClaimsPrincipalFactory;
private readonly UserManager<IdentityUser> _userManager;
private readonly RoleManager<IdentityRole> _roleManager;
public ProfileService(IUserClaimsPrincipalFactory<IdentityUser> userClaimsPrincipalFactory, UserManager<IdentityUser> userManager, RoleManager<IdentityRole> roleManager)
{
_userClaimsPrincipalFactory = userClaimsPrincipalFactory;
_userManager = userManager;
_roleManager = roleManager;
}
public async Task GetProfileDataAsync(ProfileDataRequestContext context)
{
string sub = context.Subject.GetSubjectId();
var user = await _userManager.FindByIdAsync(sub);
var userClaims = await _userClaimsPrincipalFactory.CreateAsync(user);
var claims = userClaims.Claims.ToList();
claims = claims.Where(x => context.RequestedClaimTypes.Contains(x.Type)).ToList();
claims.Add(new Claim(JwtClaimTypes.Name, "testname"));
context.IssuedClaims = claims;
}
public async Task IsActiveAsync(IsActiveContext context)
{
var sub = context.Subject.GetSubjectId();
var user = await _userManager.FindByIdAsync(sub);
context.IsActive = user != null;
}
}
and now my access token have the "name" claim
{
"iss": "https://localhost:5001",
"nbf": 1676384026,
"iat": 1676384026,
"exp": 1676387626,
"scope": [
"openid",
"profile"
],
"amr": [
"pwd"
],
"client_id": "frontend",
"sub": "c62bd642-3096-43aa-833f-966103dd3071",
"auth_time": 1676384025,
"idp": "local",
"name": "testname",
"sid": "E7E89A3E6A100C2115ED15BDE17CBE66",
"jti": "80BD171F1AD0F128DA756600D6B96DCE"
}
Should this not be added to the id token instead of the access token? It is my understanding that the id token holds the responsibility for parameters such as name, email etc?
So i guess the question is how I get profile information attached to my id token?
Tried adding profile as default for my client in my identityserver config but now profile information is added to the identity (or access) token:
public class Config
{
public static IEnumerable<IdentityResource> IdentityResources =>
new[]
{
new IdentityResources.OpenId(),
new IdentityResources.Profile(),
new IdentityResource
{
Name = "role",
UserClaims = new List<string> {"role"}
}
};
public static IEnumerable<ApiScope> ApiScopes =>
new[]
{
new ApiScope("testapi.read"),
new ApiScope("testapi.write")
};
public static IEnumerable<ApiResource> ApiResources =>
new[]
{
new ApiResource("testapi")
{
Scopes = new List<string> { "testapi.read", "testapi.write"},
ApiSecrets = new List<Secret> {new Secret("Secret".Sha256())},
UserClaims = new List<string> {"role", IdentityServerConstants.StandardScopes.Profile }
}
};
public static IEnumerable<Client> Clients =>
new[]
{
new Client
{
ClientId = "m2m.client",
ClientName = "m2m credentials client",
AllowedGrantTypes = GrantTypes.ClientCredentials,
ClientSecrets = { new Secret("ClientSecret".Sha256())},
AllowedScopes = { "testapi.read", "testapi.write" }
},
new Client
{
ClientId = "frontend",
ClientSecrets = { new Secret("ClientSecret".Sha256()) },
AllowedGrantTypes = GrantTypes.Code,
RedirectUris = { "http://localhost:3000/api/auth/callback/frontend"},
FrontChannelLogoutUri = "http://localhost:3000/signout-oidc",
PostLogoutRedirectUris = { "http://localhost:3000/signout-callback-oidc"},
AllowOfflineAccess = true,
AllowedScopes = {"openid", "profile", "testapi.read", "testapi.write" },
RequirePkce = true,
RequireConsent= false,
AllowPlainTextPkce = false
}
};
}
I'm trying to implement authentication in my Flutter app using Cognito. I'm authenticating against an existing userPool which I've been successfully using for the past year in my React app.
However, with Flutter I'm not able to fetch the user's session. I'm able to login successfully but I'm unable to get any tokens using the fetchAuthSession() method. Any idea why this is happening? Here is some of my working and non-working code:
This code is successful...
Future _usersEmail() async {
try {
var attributes = (await Amplify.Auth.fetchUserAttributes()).toList();
for (var attribute in attributes) {
if (attribute.userAttributeKey == 'email') {
print("user's email is ${attribute.value}");
return '${attribute.value}';
}
}
return 'no email';
} on AuthException catch (e) {
return '${e.message}';
}
}
This code is successful too...
Future<bool> _isSignedIn() async {
final CognitoAuthSession session =
await Amplify.Auth.fetchAuthSession() as CognitoAuthSession;
print('_isSignedIn: ${session.isSignedIn}');
return session.isSignedIn;
}
This code return null...
Future _getIdToken() async {
final CognitoAuthSession session =
await Amplify.Auth.fetchAuthSession() as CognitoAuthSession;
final idToken = session.userPoolTokens?.idToken;
print('idToken: $idToken');
return idToken;
}
Here is my amplifyconfig...
{
"UserAgent": "aws-amplify-cli/2.0",
"Version": "1.0",
"auth": {
"plugins": {
"awsCognitoAuthPlugin": {
"UserAgent": "aws-amplify-cli/0.1.0",
"Version": "0.1.0",
"IdentityManager": {
"Default": {}
},
"CredentialsProvider": {
"CognitoIdentity": {
"Default": {
"PoolId": "us-east-1_abcxyz",
"Region": "us-east-1"
}
}
},
"CognitoUserPool": {
"Default": {
"PoolId": "us-east-1_abcxyz",
"AppClientId": "5j0kii90dJ09est43xh3X21",
"Region": "us-east-1"
}
},
"Auth": {
"Default": {
"authenticationFlowType": "USER_SRP_AUTH"
}
}
}
}
}
}
you might need to set getAWSCredentials to true in your options parameter like so:
final authSession = (await Amplify.Auth.fetchAuthSession(
options: CognitoSessionOptions(getAWSCredentials: true),
)) as CognitoAuthSession;
I'm trying to secure my Quarkus API with JWT. The JWT is provided (snippet: Bearer eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUI[...] ).
The following endpoints are the 2 endpoints I've tested:
#Path("/quiz")
#RequestScoped
public class SomeResource {
#Inject
JsonWebToken jwt;
#POST
#RolesAllowed({"magister"})
#Path("/save")
#Consumes("application/json")
#Produces("*/*")
#Transactional
public Response save(#RequestBody Quiz quiz) { }
#GET
#PermitAll
#Path("/get/all")
#Produces("application/json")
public Response getAll(){ }
Both endpoints (#PermitAll and #RolesAllowed) are returning me an HTTP 401 (Unauthorized).
Do you have an idea why? I thought that #PermitAll is permitting EVERY request? Even though my token proves I have the role needed:
"resource_access" : {
"client_interface" : {
"roles" : ["magister"]
},
...
}
Edit:
Found out that the MicroProfile Spec says that
"groups":["magister"]
should get mapped by microprofile to RolesAllowed annotations.
My Payload looks like this:
{
[...]
"resource_access": {
"client_interface": {
"roles": [
"magister"
]
},
"account": {
"roles": [
"manage-account",
"manage-account-links",
"view-profile"
]
}
},
"scope": "profile email",
"email_verified": false,
"groups": [
"magister"
],
"preferred_username": "magister"
}
but I'll still get 401 Response
I had the same problem, I fixed it by adding the following code:
#OpenAPIDefinition(
info = #Info(
title = "Title API",
version = "1.0.0",
description = "Description API"
),
security = #SecurityRequirement(name = "jwt"),
components = #Components(
securitySchemes = {
#SecurityScheme(
securitySchemeName = "jwt",
description = "Token JWT",
type = SecuritySchemeType.HTTP,
scheme = "bearer",
bearerFormat = "jwt"
)
}
)
)
and also made an update Quarkus to version 1.12.0.FINAL
Generally 401 is about using a expired token, or a invalid one.
I am running my blockchain project which is election system using blockchain using hyperledger fabric and IBM blockchain Platform VSCode Extension (runnning my network locally). I am successfull to run connect to local fabric and my gateway is running successfully(image attached). Also all docker containers of peer,orderer,certificate authority are running (image attached).
connected to local fabric and gateway
all docker containers running successfuly
The problem which I am facing is that when (after connecting to local fabric and gateway) I am running invoke.js file using the command "node invoke.js" I am encountering above error. following picture link shows how I am running invoke.js
trying to run invoke.js but facing error
following is my config.json file code
{
"connection_file": "fabric_connection.json",
"appAdmin": "admin",
"appAdminSecret": "adminpw",
"orgMSPID": "org1MSP",
"caName": "ca",
"userName": "V1",
"gatewayDiscovery": { "enabled": true, "asLocalhost": true }
}
and following is my fabric-connection.json file code
{
"certificateAuthorities": {
"ca.org1.example.com": {
"caName": "ca",
"url": "http://localhost:17090"
}
},
"client": {
"connection": {
"timeout": {
"orderer": "300",
"peer": {
"endorser": "300"
}
}
},
"organization": "Org1MSP"
},
"name": "ca.org1.example.com",
"organizations": {
"Org1MSP": {
"certificateAuthorities": [
"ca.org1.example.com"
],
"mspid": "Org1MSP",
"peers": [
"Org1Peer1"
]
}
},
"peers": {
"Org1Peer1": {
"url": "grpc://localhost:17091"
}
},
"version": "1.0.0"
}
and following is my invoke.js file complete code
//Import Hyperledger Fabric 1.4 programming model - fabric-network
'use strict';
const { FileSystemWallet, Gateway } = require('fabric-network');
const path = require('path');
const fs = require('fs');
//connect to the config file
const configPath = path.join(process.cwd(), './config.json');
const configJSON = fs.readFileSync(configPath, 'utf8');
const config = JSON.parse(configJSON);
// connect to the connection file
const ccpPath = path.join(process.cwd(), './ibpConnection.json');
const ccpJSON = fs.readFileSync(ccpPath, 'utf8');
const connectionProfile = JSON.parse(ccpJSON);
// A wallet stores a collection of identities for use
const walletPath = path.join(process.cwd(), './wallet');
const wallet = new FileSystemWallet(walletPath);
console.log(`Wallet path: ${walletPath}`);
const peerIdentity = 'admin';
async function queryAll() {
try {
let response;
// Check to see if we've already enrolled the user.
const userExists = await wallet.exists(peerIdentity);
if (!userExists) {
console.log('An identity for the user ' + peerIdentity + ' does not exist in the wallet');
console.log('Run the registerUser.js application before retrying');
response.error = 'An identity for the user ' + peerIdentity + ' does not exist in the wallet. Register ' + peerIdentity + ' first';
return response;
}
//connect to Fabric Network, but starting a new gateway
const gateway = new Gateway();
//use our config file, our peerIdentity, and our discovery options to connect to Fabric network.
await gateway.connect(connectionProfile, { wallet, identity: peerIdentity, discovery: config.gatewayDiscovery });
//connect to our channel that has been created on IBM Blockchain Platform
const network = await gateway.getNetwork('mychannel');
//connect to our insurance contract that has been installed / instantiated on IBM Blockchain Platform
const contract = await network.getContract('voteChainDemo');
//submit transaction to the smart contract that is installed / instnatiated on the peers
console.log('calling contract.evaluateTransaction, with args');
response = await contract.submitTransaction('queryAll');
response = JSON.parse(response.toString());
console.log(`response from evaluateTransaction: ${(response)}`)
console.log('Transaction has been submitted');
// Disconnect from the gateway.
await gateway.disconnect();
} catch (error) {
console.error(`Failed to submit transaction: ${error}`);
}
}
// let args = ["V1"]
// args = args.toString();
queryAll();
and I see following logs when I view docker container logs of peer0.org1.example.com
enter image description here
following is the code of ibpconnection.json file
{
"name": "mychannel",
"description": "Network on IBP v2",
"version": "1.0.0",
"client": {
"organization": "org1MSP"
},
"organizations": {
"org1MSP": {
"mspid": "org1MSP",
"peers": [
"173.193.112.109:17091"
],
"certificateAuthorities": [
"173.193.112.109:7054"
]
}
},
"orderers": {
"173.193.112.109:7050": {
"url": "grpcs://173.193.112.109:17097",
"tlsCACerts": {
"pem": "-----BEGIN CERTIFICATE-----\nMIICJzCCAc6gAwIBAgIUCZxOyrvnwM/IG/3zQ9opnOE/gBEwCgYIKoZIzj0EAwIw\nZTELMAkGA1UEBhMCVVMxFzAVBgNVBAgTDk5vcnRoIENhcm9saW5hMRQwEgYDVQQK\nEwtIeXBlcmxlZGdlcjEPMA0GA1UECxMGRmFicmljMRYwFAYDVQQDEw1PcmRlcmVy\nQ0EtdGxzMB4XDTE5MDYxNDIwNDcwMFoXDTM0MDYxMDIwNDcwMFowZTELMAkGA1UE\nBhMCVVMxFzAVBgNVBAgTDk5vcnRoIENhcm9saW5hMRQwEgYDVQQKEwtIeXBlcmxl\nZGdlcjEPMA0GA1UECxMGRmFicmljMRYwFAYDVQQDEw1PcmRlcmVyQ0EtdGxzMFkw\nEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEOXI7XkoPBn7a9Q1x2S8SpmilQBalhorq\nCo96GChxQU0HJX/1qRPNN72CKx2YS/ksl+eOaHe/+pH32S5VWZLxaKNcMFowDgYD\nVR0PAQH/BAQDAgEGMBIGA1UdEwEB/wQIMAYBAf8CAQEwHQYDVR0OBBYEFIdV28CJ\nPozrl6hpxVkKpNdmAO7vMBUGA1UdEQQOMAyHBK3BcG2HBApM2GAwCgYIKoZIzj0E\nAwIDRwAwRAIgTOPmbGXzIL8SriNT/x8XdBLoTbpEVd/HIpv9nf0bWysCIBvOppOp\nvINgCydCwV1FTbP5tuqYxuShVTAba1h9ZZmm\n-----END CERTIFICATE-----\n"
}
}
},
"peers": {
"173.193.112.109:17091": {
"url": "grpcs://173.193.112.109:17093",
"tlsCACerts": {
"pem": "-----BEGIN CERTIFICATE-----\nMIICIzCCAcqgAwIBAgIUbY5U1xnvvSqJ61CgeMp9/qu448owCgYIKoZIzj0EAwIw\nYzELMAkGA1UEBhMCVVMxFzAVBgNVBAgTDk5vcnRoIENhcm9saW5hMRQwEgYDVQQK\nEwtIeXBlcmxlZGdlcjEPMA0GA1UECxMGRmFicmljMRQwEgYDVQQDEwtWb3RlckNB\nLXRsczAeFw0xOTA2MTQyMDQwMDBaFw0zNDA2MTAyMDQwMDBaMGMxCzAJBgNVBAYT\nAlVTMRcwFQYDVQQIEw5Ob3J0aCBDYXJvbGluYTEUMBIGA1UEChMLSHlwZXJsZWRn\nZXIxDzANBgNVBAsTBkZhYnJpYzEUMBIGA1UEAxMLVm90ZXJDQS10bHMwWTATBgcq\nhkjOPQIBBggqhkjOPQMBBwNCAASFv8sUAUfTvn8AJ/fiqrk0wdoMaKlG38nU6HZB\nkdUgFWZH9vnlTTBT77+GYRXuv78lg7ttus6DEAJE0X1xDd27o1wwWjAOBgNVHQ8B\nAf8EBAMCAQYwEgYDVR0TAQH/BAgwBgEB/wIBATAdBgNVHQ4EFgQUHuwEDf9d4vrv\nZM+qveoS9PV8/5cwFQYDVR0RBA4wDIcErcFwbYcECkzYYDAKBggqhkjOPQQDAgNH\nADBEAiBjynyKK+Bo4WX3wQII1nk2BU8OaYAuBvpTS/pPROdX+QIgSsLzKWuR7dFN\n50KrbM4ayRuaFBOFL88FflKxaRjQels=\n-----END CERTIFICATE-----\n"
},
"grpcOptions": {
"ssl-target-name-override": "173.193.112.109"
}
}
},
"certificateAuthorities": {
"173.193.112.109:7054": {
"url": "https://173.193.112.109:17090",
"caName": "ca",
"tlsCACerts": {
"pem": "-----BEGIN CERTIFICATE-----\r\nMIICezCCAeSgAwIBAgIJNQli68LG70HNMA0GCSqGSIb3DQEBBQUAMHUxGDAWBgNV\r\nBAMTDzE3My4xOTMuMTEyLjEwOTELMAkGA1UEBhMCVVMxFzAVBgNVBAgTDk5vcnRo\r\nIENhcm9saW5hMRAwDgYDVQQHEwdSYWxlaWdoMQwwCgYDVQQKEwNJQk0xEzARBgNV\r\nBAsTCkJsb2NrY2hhaW4wHhcNMTkwNjE0MjA0NDM2WhcNMjAwNjEzMjA0NDM2WjB1\r\nMRgwFgYDVQQDEw8xNzMuMTkzLjExMi4xMDkxCzAJBgNVBAYTAlVTMRcwFQYDVQQI\r\nEw5Ob3J0aCBDYXJvbGluYTEQMA4GA1UEBxMHUmFsZWlnaDEMMAoGA1UEChMDSUJN\r\nMRMwEQYDVQQLEwpCbG9ja2NoYWluMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKB\r\ngQCXKBfHLyQfavboQU0y/3S4jlqh6vQgXZKeAMliXfigf6aLG/3Oc4pxuQxBccB6\r\nAiYTFZdShTy2Usx5GsDf5PWxfD4vJ8FWzAGlIYmVqseKXXjsQvwMlCMyS9K2NaDo\r\n9tXwvHz8Sgncq7KccseVYwX4FFpSQWZsIV27Y2xkMZ9bVQIDAQABoxMwETAPBgNV\r\nHREECDAGhwStwXBtMA0GCSqGSIb3DQEBBQUAA4GBAG1+VZNSQdm0DX9CbZzx9zbx\r\nnTEHyrhVTgZs5YuUvZX8BErYfJFxuPBsXhOpQan/L9y+a7C/Caac4WJ/l/l34e5M\r\nG1Hn603wkHpR0UFuGCikdctm+6iHUVro5CMfQlEPIqaJSTFb7Ju5aeeerHnkvQx8\r\nBShP1pNsvsyOctmFhQCQ\r\n-----END CERTIFICATE-----\r\n"
}
}
}
}
In my project which is pretty much having the same things as you, I did not specified the orderers in my connection config file. I am not sure if it is required in your scenario.
Besides, I think the issue is caused by incorrect urls specified in your ibpconnection.jsonfile.
orderers should have "url": "grpcs://173.193.112.109:7050"
AND
peers should have "url": "grpcs://173.193.112.109:17091"
Hope this will fix the error. Good luck!!!!
I'm a bit confused by the Google Actions documentation about storing data and hoped someone can help clarify...
The docs state that data in the conv.user.storage object will be saved "across conversations". I took this to mean that if the user exited the conversation these values would be persisted and available the next time they interact with my action. Is that understanding correct?
The reason I ask is that I can't get this behaviour to work in my action.
I have built a simple action fulfilment service (using Actions on Google NodeJS library v2.4.0 and Koa v2.5.3). The fulfilment is triggered from an intent defined in Dialogflow (after an account has been linked with Google Sign In) and stores a value in conversation storage. The code is as follows:
server.js (base server - loads actions dynamically from the local ./actions/ dir)
/* Load the environment */
const dotenv = require('dotenv');
const path = require('path');
const packageJson = require('./package.json');
dotenv.config({
silent: true,
path: process.env.ENV_FILE!=undefined && process.env.ENV_FILE.trim()!='' ? path.normalize(process.env.ENV_FILE) : path.join(__dirname, './.env')
});
const SERVER_NAME = process.env.NAME || packageJson.name;
const SERVER_PORT = process.env.PORT||'8080';
const SERVER_HOST = process.env.HOST||'0.0.0.0';
const HANDLERS_PATH = './actions/';
/* Load the dependencies */
const logger = require('utils-general').logger('google-server');
const Koa = require('koa');
const KoaBody = require('koa-body');
const KoaActionsOnGoogle = require('koa-aog');
const fs = require('fs');
const { dialogflow } = require('actions-on-google');
/* Load and initialise the Google Assistant actions */
//Initialise DialogFlow
const action = dialogflow({ debug: process.env.ACTIONS_DEBUG==='true', clientId: process.env.GOOGLE_CLIENT_ID });
//Load the action intent handlers
const handlers = [];
let handlerFiles = fs.readdirSync(HANDLERS_PATH);
handlerFiles.forEach(function loadHandlers(file) {
let handlerImpl = require(HANDLERS_PATH+file);
let handler = {};
handler[handlerImpl.intent] = handlerImpl.action;
handlers.push(handler);
});
//Add the actions intent handlers to DialogFlow
handlers.forEach(item => {
let key = Object.keys(item)[0];
logger.info(`Adding handler for action intent ${key}`);
action.intent(key, item[key]);
});
/* Create the application server to handle fulfilment requests */
logger.info(`Initialising the ${SERVER_NAME} server (port: ${SERVER_PORT}, host: ${SERVER_HOST})`);
//Create the server
const app = new Koa();
//Add default error handler middleware
app.on('error', function handleAppError(err) {
logger.error(`Unhandled ${err.name||'Error'}: ${err.message || JSON.stringify(err)}`);
});
//Add body parsing middleware
app.use(KoaBody({ jsonLimit: '50kb' }));
//Log the request/ response
app.use(async (ctx, next) => {
logger.trace(`REQUEST ${ctx.method} ${ctx.path} ${JSON.stringify(ctx.request.body)}`);
await next();
logger.trace(`RESPONSE (${ctx.response.status}) ${ctx.response.body ? JSON.stringify(ctx.response.body) : ''}`);
});
//Make the action fulfilment endpoint available on the server
app.use(KoaActionsOnGoogle({ action: action }));
/* Start server on the specified port */
app.listen(SERVER_PORT, SERVER_HOST, function () {
logger.info(`${SERVER_NAME} server started at ${new Date().toISOString()} and listening for requests on port ${SERVER_PORT}`);
});
module.exports = app;
storage-read.js (fulfilment for the "STORAGE_READ" intent - reads stored uuid from conversation storage):
const logger = require('utils-general').logger('google-action-storage-read');
const { SimpleResponse } = require('actions-on-google');
const { getUserId } = require('../utils/assistant-util');
const _get = require('lodash.get');
module.exports = {
intent: 'STORAGE_READ',
action: async function (conv, input) {
logger.debug(`Processing STORAGE_READ intent request: ${JSON.stringify(conv)}`, { traceid: getUserId(conv) });
let storedId = _get(conv, 'user.storage.uuid', undefined);
logger.debug(`User storage UUID is ${storedId}`);
conv.close(new SimpleResponse((storedId!=undefined ? `This conversation contains stored data` : `There is no stored data for this conversation`)));
}
}
storage-write.js (fulfils the "STORAGE_WRITE" intent - writes a UUID to conversation storage):
const logger = require('utils-general').logger('google-action-storage-read');
const { SimpleResponse } = require('actions-on-google');
const { getUserId } = require('../utils/assistant-util');
const _set = require('lodash.set');
const uuid = require('uuid/v4');
module.exports = {
intent: 'STORAGE_WRITE',
action: async function (conv, input) {
logger.debug(`Processing STORAGE_WRITE intent request`, { traceid: getUserId(conv) });
let newId = uuid();
logger.debug(`Writing new UUID to conversation storage: ${newId}`);
_set(conv, 'user.storage.uuid', newId);
conv.close(new SimpleResponse(`OK, I've written a new UUID to conversation storage`));
}
}
This "STORAGE_WRITE" fulfilment stores the data and makes it available between turns in the same conversation (i.e. another intent triggered in the same conversation can read the stored data). However, when the conversation is closed, subsequent (new) conversations with the same user are unable to read the data (i.e. when the "STORAGE_READ" intent is fulfilled) - the conv.user.storage object is always empty.
I have voice match set up on the Google account/ Home Mini I'm using, but I can't see how I determine in the action if the voice is matched (although it seems to be as when I start a new conversation my linked account is used). I'm also getting the same behaviour on the simulator.
Sample request/ responses (when using the simulator) are as follows:
STORAGE_WRITE request:
{
"user": {
"userId": "AB_Hidden_EWVzx3q",
"locale": "en-US",
"lastSeen": "2018-10-18T12:52:01Z",
"idToken": "eyMyHiddenTokenId"
},
"conversation": {
"conversationId": "ABwppHFrP5DIKzykGIfK5mNS42yVzuunzOfFUhyPctG0h0xM8p6u0E9suX8OIvaaGdlYydTl60ih-WJ5kkqV4acS5Zd1OkRJ5pnE",
"type": "NEW"
},
"inputs": [
{
"intent": "actions.intent.MAIN",
"rawInputs": [
{
"inputType": "KEYBOARD",
"query": "ask my pathfinder to write something to conversation storage"
}
],
"arguments": [
{
"name": "trigger_query",
"rawText": "write something to conversation storage",
"textValue": "write something to conversation storage"
}
]
}
],
"surface": {
"capabilities": [
{
"name": "actions.capability.WEB_BROWSER"
},
{
"name": "actions.capability.AUDIO_OUTPUT"
},
{
"name": "actions.capability.SCREEN_OUTPUT"
},
{
"name": "actions.capability.MEDIA_RESPONSE_AUDIO"
}
]
},
"isInSandbox": true,
"availableSurfaces": [
{
"capabilities": [
{
"name": "actions.capability.WEB_BROWSER"
},
{
"name": "actions.capability.AUDIO_OUTPUT"
},
{
"name": "actions.capability.SCREEN_OUTPUT"
}
]
}
],
"requestType": "SIMULATOR"
}
STORAGE_WRITE response:
{
"conversationToken": "[]",
"finalResponse": {
"richResponse": {
"items": [
{
"simpleResponse": {
"textToSpeech": "OK, I've written a new UUID to conversation storage"
}
}
]
}
},
"responseMetadata": {
"status": {
"message": "Success (200)"
},
"queryMatchInfo": {
"queryMatched": true,
"intent": "a7e54fcf-8ff1-4690-a311-e4c6a8d1bfd7"
}
},
"userStorage": "{\"data\":{\"uuid\":\"7dc835fa-0470-4028-b8ed-3374ed65ac7c\"}}"
}
Subsequent STORAGE_READ request:
{
"user": {
"userId": "AB_Hidden_EWVzx3q",
"locale": "en-US",
"lastSeen": "2018-10-18T12:52:47Z",
"idToken": "eyMyHiddenTokenId"
},
"conversation": {
"conversationId": "ABwppHHVvp810VEfa4BhBJPf1NIfKUGzyvw9JCw7kKq9YBd_F8w0VYjJiSuzGLrHcXHGc9pC6ukuMB62XVkzkZOaC24pEbXWLQX5",
"type": "NEW"
},
"inputs": [
{
"intent": "STORAGE_READ",
"rawInputs": [
{
"inputType": "KEYBOARD",
"query": "ask my pathfinder what is in conversation storage"
}
],
"arguments": [
{
"name": "trigger_query",
"rawText": "what is in conversation storage",
"textValue": "what is in conversation storage"
}
]
}
],
"surface": {
"capabilities": [
{
"name": "actions.capability.WEB_BROWSER"
},
{
"name": "actions.capability.AUDIO_OUTPUT"
},
{
"name": "actions.capability.SCREEN_OUTPUT"
},
{
"name": "actions.capability.MEDIA_RESPONSE_AUDIO"
}
]
},
"isInSandbox": true,
"availableSurfaces": [
{
"capabilities": [
{
"name": "actions.capability.WEB_BROWSER"
},
{
"name": "actions.capability.AUDIO_OUTPUT"
},
{
"name": "actions.capability.SCREEN_OUTPUT"
}
]
}
],
"requestType": "SIMULATOR"
}
STORAGE_READ response:
{
"conversationToken": "[]",
"finalResponse": {
"richResponse": {
"items": [
{
"simpleResponse": {
"textToSpeech": "There is no stored data for this conversation"
}
}
]
}
},
"responseMetadata": {
"status": {
"message": "Success (200)"
},
"queryMatchInfo": {
"queryMatched": true,
"intent": "368d08d3-fe0c-4481-aa8e-b0bdfa659eeb"
}
}
}
Can someone set me straighten me out on whether I'm misinterpreting the docs or maybe I have a bug somewhere?
Thanks!
my suspicion is that personal results are turned off in your case.
You mentioned you're testing on Home Mini and Prisoner was able reproduce on device (in the comments).
Shared devices like Smart Speakers (Home, Mini) and Smart Displays have personal results disabled by default. Check this documentation to enable it.
Open Settings on your Android phone
Under "Assistant devices," select your device (e.g. Mini)
Turn Personal results on
Beware that this means personal results like Calendar entries can be accessed through the device.
To check if userStorage will persist, you can use the GUEST/VERIFIED flag, see documentation here.