@auth directive status and security

Hi!

I wanted to follow up and check in on the status of the full @auth directive support on all GraphQL types - has this feature made it into the official release branches yet? Last I heard, there were tentative plans for it to be more fully available by the end of this month.

Also, as a related question, since I’ll be using Dgraph as the entirety of the backend, how secure will the database be? Given resources like @auth and @custom will be available, how much, if any, additional “wrapping” will be needed say by a Golang server with middlware? Ideally, if I could just strip it all out and rely on Auth0 to sync with the Dgraph directives, that’d be amazing. I’m planning on having both a “frontend”, providing a UI, and “internal”, for more programmatic features and responding to GraphQL subscriptions, app point at Dgraph (the “backend”).

Best,
John

1 Like

Hey @forstmeier

@auth directive now has pretty comprehensive support now. We have a bunch of examples for it on the docs site at https://graphql.dgraph.io/authorization/. The support is already in master and would be part of the 20.07.0 release that goes out next month.

That’s exactly why we built these features. So that you shouldn’t have to wrap Dgraph behind another Golang server. Let us know if something doesn’t work for you and if end up having to use a proxy server. We’ll look into it and see if we can make the interaction smoother for you.

1 Like

Hi @pawan!

Thanks for the quick update! I’ll download the most recent version and see if I can start playing with it then. I’ll also strip out the “backend” code I currently have in place to see if it is comprehensive enough for what I’m looking for (it’ll be connecting to Auth0 and I’ve seen examples of this floating around on both your and their documentations).

Best,
John

1 Like

P.S. If I’m including a series of filters via the @auth directive (e.g authentication, authorization, and multi-tenancy), how can I filter for the later based on an org ID (which in this case is the scalar ID? I’m trying to us this:

		{ rule: """query($orgID: ID!) {
			queryConsent {
				owner( filter: { id: { eq: $orgID } } ) {
					id
				}
			}
		}"""},

to provide multi-tenancy around the Consent type document, but Dgraph is throwing an error indicating, from what I can tell, that eq is incompatible with the ID type. E.g. @auth: failed to validate GraphQL rule [reason : Value provided {eq:$orgID} is incompatible with expected type [ID!]]. I dug through the documentation but couldn’t find an answer - did I miss something?

If you look at your graphql schema it is probably:

filter: { id: [$orgID] }
1 Like

Another quick thing, in this similar thread: given that I’ll be basically exposing Dgraph as my backend API in its entirety, I’m going to need to be able to do some user role management in Auth0 (my user management app of choice for the project).

For example, once a user is added to the Dgraph database, I assume I’d want to have something like a @custom directive shoot off an API request to Auth0 to update the user database. Similarly, a user’s role was being changed, or some other data regarding the user for that matter, I’d potentially want that reflected in Auth0.

Would the @custom directive be the recommended tool in this case? And if so, would it be to directly invoke the Auth0 API or some sort of intermediary of my own (given there might be some different scenarios that the @custom directive couldn’t handle)?

I guess you are talking about triggers. Triggers do all this automatically. And, in your case, it would be like a custom trigger, which can fire some API requests too. At present, triggers don’t exist anywhere in dgraph.

But, I guess you could still do it by making an explicit query with custom fields. So, lets say you have this schema:

type User {
  id: ID!
  name: String!
  dob: DateTime
  roles: [Role]
  saveToAuthZero: AuthZeroAddResponse! @custom(http: {
        url: "https://auth0url/addUser"
        method: "POST"
        body: "{uid: $id, name: $name, dob: $dob}"
        mode: SINGLE
        forwardHeaders: ["Auth0-Token"]
  })
  updateInAuthZero: AuthZeroUpdateResponse! @custom(http: {
        url: "https://auth0url/updateUser/$id"
        method: "PUT"
        body: "{roles: $roles}"
        mode: SINGLE
        forwardHeaders: ["Auth0-Token"]
  })
}
type Role {
  id: ID!
  permission: Int!
  on: String!
  ...
}
type AuthZeroAddResponse { ... }
type AuthZeroUpdateResponse { ... }

Then, when you have saved a user in dgraph, you can query the custom field saveToAuthZero for that user, like this:

query {
  getUser(id: "0x3") {
    saveToAuthZero {
      ...
    }
  }
}

It would end up calling the specified Auth0 API with the specified body in custom directive. This is just an idea. Similarly, you could query the field updateInAuthZero when you want to update things in Auth0.
Notice, that while querying you can send a header named Auth0-Token to dgraph, which will be forwarded as-is to Auth0 servers. You would want to probably use that for sending your JWT to Auth0.

So, what I just showed above is like a manual trigger instead of automated triggers. If you want to trigger such things manually, then you can choose @custom for your use-case. Automated triggering is not possible at present.

I guess you can do it directly. You would just need to map the Auth0 response payload to the types in GraphQL schema. As, you can forward headers, there shouldn’t be any problem.

2 Likes

@abhimanyusinghgaur the @custom directive as “triggers” is the way I’m going however I’m running into a snag trying to get this particular type to update.

type User @auth( // removed for brevity // ) {
	owner: OwnerOrg!
	email: String! @id
	firstName: String! @search(by: [exact])
	lastName: String! @search(by: [exact])
	role: Role!
	org: Org! @hasInverse(field: users)
	user_id: String!
	createAuth0User: User @custom(http: {
		url: "https://folivora.auth0.com/api/v2/users",
		method: POST,
		body: "{ email: $email, app_metadata: { role: $role, orgID: $owner }, given_name: $firstName, family_name: $lastName, connection: \"Username-Password-Authentication\" }"
		forwardedHeaders: ["Authorization"]
	})
	updateAuth0User: User @custom(http: {
		url: "https://folivora.auth0.com/api/v2/users/$user_id",
		method: PATCH,
		body: "{ email: $email, app_metadata: { role: $role, orgID: $owner }, given_name: $firstName, family_name: $lastName }",
		forwardedHeaders: ["Authorization"]
	})
	deleteAuth0User: User @custom(http: {
		url: "https://folivora.auth0.com/api/v2/users/$user_id",
		method: DELETE,
		forwardedHeaders: ["Authorization"]
	})
}

I’m getting this error back:

{"errors":[{"message":"resolving updateGQLSchema failed because input:1: Type User; Field createAuth0User; body template inside @custom directive could not be parsed.\ninput:1: Type User; Field createAuth0User: @custom directive, body template must use a field with type ID! or a field with @id directive.\ninput:1: Type User; Field updateAuth0User; body template inside @custom directive could not be parsed.\ninput:1: Type User; Field updateAuth0User: @custom directive, body template must use a field with type ID! or a field with @id directive.\n (Locations: [{Line: 3, Column: 4}])","extensions":{"code":"Error"}}]}

This is weird because the @id directive is present on the email field - am I missing a typo somewhere?

Best,
John

1 Like

Hi @forstmeier,
Sorry for the late response.

There are a few limitations with @custom at present:

  • The keys in the body can only contain [a-zA-Z], things like app_metadata which have _ (underscore) in their name, won’t work for now.
  • Hardcoded values are not supported at present. So, the following won’t work.
  • Non-scalar variables are not supported in the body at present. So, $role or $owner won’t work for now.

These are the few important bits which are missing at the moment. So, that will limit you for now. We are working on adding these things in the meantime.

We could surely improve the error message because it complained about the @id field not being present. It could just be saying about not being able to parse the body. I will make a note of this.

1 Like

@abhimanyusinghgaur yes, definitely, the error message could be clearer.

And thanks for the heads up on those limitations - I expect those are on the roadmap to be addressed? It’s not a huge concern for me at the moment, but it will be necessary going down the road for what I’m working on.

1 Like

Yes, they are on our roadmap. You can expect them by mid August.

1 Like

Just as a heads up, it looks like the documentation has conflicting guidance as far as the @auth configuration goes.

The authorization page seems to show configuration as a JSON-esque structure while the TODO tutorial shows as a string-esque structure.

I pulled down the master branch and tried the JSON but I’m running into errors with "mutation failed because authorization failed". What can I share from my Auth0/Dgraph config to help try and resolve this issue?

1 Like

Thank you @forstmeier. We are in the process of updating our docs, you got us there!

The string one is old format and is deprecated now, the JSON one is the new format and is going to be supported.

Please share

  • your schema with all necessary @auth bits
  • JWT structure (if your JWT contains sensitive info, please DM it to me instead of sharing it here)
  • The mutation you were trying to perform

Excellent, I’ll use that JSON-styled config.

Here’s the relevant part of the schema:

interface Org {
	id: ID!
	name: String! @search(by: [hash])
	users: [User!]! @hasInverse(field: org)
	createdOn: DateTime!
	updatedOn: DateTime!
}

interface Location {
	street: String! @search(by: [fulltext])
	city: String! @search(by: [fulltext])
	county: String! @search(by: [exact])
	state: String! @search(by: [exact])
	country: String! @search(by: [exact])
	zip: Int! @search
}

type OwnerOrg implements Org & Location @auth(
	add: { and: [
		{ rule: "{ $isAuthenticated: { eq: \"true\" } }" },
		{ rule: "{ $role: { eq: \"USER_ADMIN\" } }"},
	]},
	query: { and: [
		{ rule: "{ $isAuthenticated: { eq: \"true\" } }" },
		{ or: [
			{ rule: "{ $role: { eq: \"USER_ADMIN\" } }"},
			{ rule: "{ $role: { eq: \"USER_INTERNAL\" } }"},
		]},
		{ rule: """query($orgID: ID!) {
			queryOwnerOrg( filter: { id: [$orgID] } ) {
				id
			}
		}"""},
	]},
	get: { and: [
		{ rule: "{ $isAuthenticated: { eq: \"true\" } }" },
		{ or: [
			{ rule: "{ $role: { eq: \"USER_ADMIN\" } }"},
			{ rule: "{ $role: { eq: \"USER_INTERNAL\" } }"},
		]},
		{ rule: """query($orgID: ID!) {
			queryOwnerOrg( filter: { id: [$orgID] } ) {
				id
			}
		}"""},
	]},
	update: { and: [
		{ rule: "{ $isAuthenticated: { eq: \"true\" } }" },
		{ rule: "{ $role: { eq: \"USER_ADMIN\" } }"},
		{ rule: """query($orgID: ID!) {
			queryOwnerOrg( filter: { id: [$orgID] } ) {
				id
			}
		}"""},
	]},
	delete: { and: [
		{ rule: "{ $isAuthenticated: { eq: \"true\" } }" },
		{ rule: "{ $role: { eq: \"USER_ADMIN\" } }"},
		{ rule: """query($orgID: ID!) {
			queryOwnerOrg( filter: { id: [$orgID] } ) {
				id
			}
		}"""},
	]},
) {
	labs: [LabOrg!]! @hasInverse(field: owner)
	storages: [StorageOrg!]! @hasInverse(field: owner)
}

# Dgraph.Authorization {"VerificationKey":"-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAt50KaKOwI1/r9yEojzVW\ncOwGTZbL7sjlUaSI25icLPF8eK1R2dbVaKTdZNtq6LAxFe+NDt2AuU7Vtqzv8GGv\nb2RP5KEgUcJyy75Yw0hT4TP3SrzDB2paCfcKHxQlTQ0pFP0SJMk4YCfq+gDqPnXQ\nCfzw+Zff29zZh5bs1lOxvAIgsu9LtH/zX6f5ASMdHV8EPWdZq6nq8KoOiMcAizDj\nrbm/qcAJP6k+ztbgtN6HdD8v6+7uIKStrYRa0BLXdJAra2uaLI4z2H22RHuzhkIu\nytxpYnxDlYTXzroSiRs/vs/dyHixT8smbEQmLoPTpflnoEZcNDXkhf0v9yVtG6NV\n1QIDAQAB\n-----END PUBLIC KEY-----","Header":"X-Auth0-Token","Namespace":"https://sideproject.io/jwt/claims","Algo":"RS256"}

This is the claims section of the JWT:

{
  "https://folivora.io/jwt/claims": {
    "isAuthenticated": true,
    "role": "USER_ADMIN",
    "orgID": "42"
  },
  "nickname": "john.forstmeier"
}

And the mutation:

mutation AddOwnerOrgs($input: [AddOwnerOrgInput!]!) { addOwnerOrg(input: $input) { id } }

Thanks!

true is string in schema rule, while it is boolean in JWT. You will need to change the true in JWT to string like "true" because you can’t change it in schema rule :slight_smile:

Another thing:

while in JWT it is:

Both should be same :slight_smile:

And, specifying get rules is not supported, the query ones will apply there.

1 Like

Good catch on the "isAuthenticated" string vs Boolean, @abhimanyusinghgaur! Also, the claims key is updated to match in both locations and I removed the get from the rules but I’m continuing to encounter the same error. The problem is I’m not really able to ascertain which rule or why there is an authorization error. The "message": "mutation failed because authorization failed" isn’t particularly helpful although I get not wanting to reveal too much in the message.

Here are the updated JWT claims:

  "https://folivora.io/jwt/claims": {
    "isAuthenticated": "true",
    "role": "USER_ADMIN",
    "orgID": "42"
  },

I had tried it with HS256 as the alogrithm, and it was working. Haven’t tried it with RS256. Are you sure the public key you have mentioned as the VerificationKey is the public key for the private key which was used to encrypt the JWT? And, is the header name same as what Auth0 sends?

Maybe @arijit can help more.

Hm, interesting. I haven’t tried it with HS256 but was hoping to stick with RS256 since it’s recommended by Auth0.

I was following the Dgraph-Auth0 resource provided by your team (with the only change being the config in the schema is represented as a JSON object now). I included the full public key (e.g. -----BEGIN PUBLIC KEY----- content and replaced newlines with \n). I set the header to the value of the header in the request I’m sending from Postman which contains the JWT I received from the SPA Auth0 Application I built/logged into.

Even I was able to query with HS256 . I thought the issue might be due to having a boolean RBAC rule which @abhimanyusinghgaur pointed out. I will try it with RS256 and get back to you.

I tried the mutation with RS256 and it’s working for me.

Schema:

type User {
    username: String! @id
    org: [Org]
}

interface Org {
	id: ID!
	name: String! @search(by: [hash])
	users: [User!]! @hasInverse(field: org)
	createdOn: DateTime!
	updatedOn: DateTime!
}

interface Location {
	street: String! @search(by: [fulltext])
	city: String! @search(by: [fulltext])
	county: String! @search(by: [exact])
	state: String! @search(by: [exact])
	country: String! @search(by: [exact])
	zip: Int! @search
}

type OwnerOrg implements Org & Location @auth(
	add: { and: [
		{ rule: "{ $isAuthenticated: { eq: \"true\" } }" },
		{ rule: "{ $role: { eq: \"USER_ADMIN\" } }"},
	]},
	query: { and: [
		{ rule: "{ $isAuthenticated: { eq: \"true\" } }" },
		{ or: [
			{ rule: "{ $role: { eq: \"USER_ADMIN\" } }"},
			{ rule: "{ $role: { eq: \"USER_INTERNAL\" } }"},
		]},
		{ rule: """query($orgID: ID!) {
			queryOwnerOrg( filter: { id: [$orgID] } ) {
				id
			}
		}"""},
	]},
	update: { and: [
		{ rule: "{ $isAuthenticated: { eq: \"true\" } }" },
		{ rule: "{ $role: { eq: \"USER_ADMIN\" } }"},
		{ rule: """query($orgID: ID!) {
			queryOwnerOrg( filter: { id: [$orgID] } ) {
				id
			}
		}"""},
	]},
	delete: { and: [
		{ rule: "{ $isAuthenticated: { eq: \"true\" } }" },
		{ rule: "{ $role: { eq: \"USER_ADMIN\" } }"},
		{ rule: """query($orgID: ID!) {
			queryOwnerOrg( filter: { id: [$orgID] } ) {
				id
			}
		}"""},
	]},
) {
    ownerName: String @search(by: [exact, term, fulltext, regexp])
}

Mutation:

		mutation AddOwnerOrgs($input: [AddOwnerOrgInput!]!) {
		  	addOwnerOrg(input: $input) { 
				numUids
				ownerOrg {
			  		id
			  		name
			  		__typename
				}
		  	} 
		}

Variable:

{
  "input" : [{
    "name" : "Arijit",
    "street" : "random",
    "city" : "random",
    "county": "random",
    "state": "random",
    "country": "random",
    "zip": 11111,
    "createdOn": "2020-01-01",
    "updatedOn": "2019-01-01",
    "users" : [ {"username" : "arijit"}]
  }]
}

JWT payload:

{
  "sub": "1234567890",
  "name": "John Doe",
  "admin": true,
  "iat": 1626239022,
  "https://xyz.io/jwt/claims": {
    "isAuthenticated": "true",
    "role": "USER_ADMIN",
    "orgID": "42"
  },
  "nickname": "john.forstmeier"
}

Result:

{
 "data": {
    "addOwnerOrg": {
      "numUids": 1,
      "ownerOrg": [
        {
          "id": "0x60",
          "name": "Arijit",
          "__typename": "OwnerOrg"
        }
      ]
    }
  }
}