Skip to content

Mobile apps

Marie opens the leave app on her phone. The app sends her to the workflow site, in the phone's browser, and she signs in there with her own password. Back in the app, she sees the two requests waiting for her approval, and approves one. The workflow site never told the app her password, and the app holds nothing that could act for anybody else.

This page shows how to build such an app on Orchestra, with no server of its own in between.

How it works

The app signs in with standard OAuth: the authorization code grant with PKCE, through a public consumer, in the system browser (RFC 8252). The app keeps the access token and the refresh token in the device's keychain.

Every call the app makes with the access token is made for Marie:

  • The token is the identity proof, of the token scheme. The app sends no actor_proof or initiator_proof.
  • Orchestra checks Marie's own permissions, as its pages do. A request she may not see is invisible to her, and a change she may not make is refused.
  • The token's scope caps what it can do. A token with the orchestra scope carries only the permissions the Orchestra API needs, however much more the person may do on the site.
  • The tenant is the consumer's, as for any caller.

A calling site, which has a server and its own client credentials, keeps working as it does today. See Distributed execution.

Setting up the workflow site

The consumer

Add a consumer for the app, under Configuration > Web services > Consumers:

Field Value
Client ID leave-app
Is Confidential? Unchecked. An app cannot keep a secret.
Use PKCE? Checked.
Redirect URIs The app's universal link (iOS) or app link (Android), such as https://leave.example.com/oauth/callback.
Grant types Authorization Code and Refresh Token.
Scopes orchestra
Orchestra tenant The tenant the app works in.
Identity proofs accepted The person, signed in through OAuth.

A public consumer cannot get a token of its own: the client credentials grant needs a secret. So every call the app makes is made for a person.

The API refuses a token a person signed in for with a 403 when its consumer is not granted The person, signed in through OAuth. So a token that another app of the site got for its own purposes cannot reach the Orchestra API.

The scope

Orchestra Server ships the orchestra scope. It groups one scope for each permission the API needs:

Scope Permission
orchestra_access_api Access the Orchestra HTTP API (read)
orchestra_drive_api Drive the Orchestra HTTP API (write)
orchestra_access_instances Access Orchestra process instances
orchestra_administer Administer Orchestra
orchestra_process_tasks Process Orchestra tasks
orchestra_view_own_instances View own Orchestra process instances

A token's permissions are the person's own, kept to those its scope names. So the scope never gives Marie a permission she does not have. It only keeps the app's token away from everything else she may do on the site.

On a site that already had Orchestra Server installed, the scopes are not there yet. Install what Orchestra Server ships, and nothing else:

drush php:eval '$directory = \Drupal::service("extension.list.module")->getPath("orchestra_server") . "/config/optional";
\Drupal::service("config.installer")->installOptionalConfig(new \Drupal\Core\Config\FileStorage($directory));'

The people

Marie needs these permissions, in one of her roles:

  • Access the Orchestra HTTP API (read) and Drive the Orchestra HTTP API (write). Only the API's routes use them, and only with an OAuth token, so they open nothing else.
  • Process Orchestra tasks, to see and complete her tasks.
  • View own Orchestra process instances, to see the requests she started.
  • Grant OAuth2 codes, from Simple OAuth, to approve the app on the site's authorize page. It grants nothing else. Out of the box only administrators have it, and no scope can give it: the site checks it at the authorize step, on Marie's own session, before any token exists.

Access Orchestra process instances lets a person read every instance of the tenant, through the app as on the instance pages. Grant it only to the people who may, such as the operators of a service, not to everyone using the app.

Signing in

Use a standard library such as AppAuth. It opens the browser on the authorization endpoint:

https://workflows.example.com/oauth/authorize
  ?response_type=code
  &client_id=leave-app
  &redirect_uri=https://leave.example.com/oauth/callback
  &scope=orchestra
  &code_challenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM
  &code_challenge_method=S256
  &state=$STATE

Marie signs in and approves. The browser returns to the app with a code, and the app exchanges it for tokens:

curl -X POST https://workflows.example.com/oauth/token \
  -d grant_type=authorization_code \
  -d client_id=leave-app \
  -d code=$CODE \
  -d redirect_uri=https://leave.example.com/oauth/callback \
  -d code_verifier=$CODE_VERIFIER

The answer holds an access_token and a refresh_token. Keep both in the keychain, never in plain storage.

Calling the API

Send the access token as the bearer, and nothing else about Marie.

Her tasks:

curl -X POST https://workflows.example.com/orchestra-api/tasks \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{"limit": 20}'

Approving one, with the resume handle the list gave:

curl -X POST https://workflows.example.com/orchestra-api/step/resume \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{"resume_handle": "'"$HANDLE"'", "payload": "approved"}'

Starting a request, which belongs to Marie:

curl -X POST https://workflows.example.com/orchestra-api/process/leave_request/start \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{"variables": {"days": 3}}'

A call that adds an actor_proof or an initiator_proof is refused with a 400: the token already says who the person is, and it can name nobody else.

The operations that act for the caller alone, such as signaling a token, the handshake and the job endpoints, refuse a person's token with a 403. The OpenAPI document says which operations take one, in x-orchestra-person-token.

Renewing the token

An access token lives as long as the consumer's Access token expiration time says. Once it has expired, Simple OAuth answers a call with a 401. Refresh it, then call again:

curl -X POST https://workflows.example.com/oauth/token \
  -d grant_type=refresh_token \
  -d client_id=leave-app \
  -d refresh_token=$REFRESH_TOKEN

Each refresh hands back a new refresh token, and the old one is spent. When the refresh is refused, the person signs in again.

Following a request

A calling site is told when a request it started changes: it gives the workflow site an address to send notices to. An app has no such address. So it reads what it shows, when it shows it:

  • Marie's work items, from /orchestra-api/work-items: her tasks, and the operations she pulls, as her My actions page lists them.
  • The requests she started, from /orchestra-api/requests, as her My requests page lists them.
  • One of those instances, as its page shows it to Marie, from /orchestra-api/instance/{instance_id}/view, sent as a POST with the token: its status, the statuses it has passed through, the steps it waits on, and the data she submitted. She sees an instance she started or works on, and no other.
  • For a person granted Access Orchestra process instances, any instance of the tenant and its variables, from /orchestra-api/instance/{instance_id}, sent as a POST with the token.

Read them when the app opens, and when Marie pulls to refresh. To wake the app when a task arrives, the workflow site can send a push notification through a delivery channel of your own, next to email. Keep the notice to "something changed", and let the app read the rest from the API with Marie's token.