Summary: Accessing my local gym requires a smartphone application, scanning two QR codes, and granting location permissions. I've figured out the unlocking mechanism and used Apple Shortcuts to unlock the gym doors with a single tap. This completely bypasses the application, and does not require me to reveal my actual location.
The access control mechanism at my local gym is a sort of mantrap. There's a small space with two interlocking doors, where the first door has to be closed before the second one can be opened. It's in place to avoid piggybacking or tailgating, but I'm skeptical it does anything but being annoying.
Each of the two doors have an associated QR code which you're meant to scan with a smartphone camera and the gym's application. This works passably, until there's either technical issues, and it's impossible to get in to the gym, or until there's a power outage and you're stuck in the mantrap.
If you're like me, you start to wonder what's on those QR codes.
IQ4aP5xLB0Dn36BKmcWz.P9tjKDTkjBE1oSU2L3jJ.These look like 20 character random alpha-numeric identifiers. I guess we'll have to figure out what the application is doing with these codes. I'm using the Proxyman application on iOS to capture the HTTPS traffic, and then the desktop application for further analysis. (Charles is also excellent.)
Looking at the captures, we can see the gym is using the Twiik third-party platform. Since Twiik has their Rest API documentation publicly available (or at least parts of it), figuring out what's going on is easier than you might initially guess.
If we start from a logged out state, the application starts with a login
request. The examples here are with
Nushell using the
http
commands. If you have some HTTP familiarity, these should easily translate
to curl or other means of making HTTP requests.
let params = [
'grant_type=password'
'username=<email>'
'password=<password>'
'client_id=<gym>_cvbmir5p77s96dyxzl9vgm2geeghao'
'client_secret=lktfpc0css5kloo5zzrmddwiswsno6drav'
] | str join '&'
let url = $'https://<gym>.api.twiik.me/v3/oauth/token?($params)'
http post --content-type application/json $url {}
From the response, we can observe we're dealing with OAuth 2.0 refresh tokens and that tokens are valid for 2 weeks (1 209 600 seconds). This means we'll have to manage state for the refresh.
╭───────────────┬────────────────────────────────────────────────────╮
│ access_token │ t3t4noi1ppikvzfpctiymaxxwdw03xor2uhawmu0a1e5qld53j │
│ token_type │ bearer │
│ expires_in │ 1209600 │
│ expire_time │ 1763798862431 │
│ refresh_token │ lnvsa4tx4jrjuln5xyn0g4bbv07vihdlodg0dd6nae3apabzsz │
│ scope │ │
│ state │ │
╰───────────────┴────────────────────────────────────────────────────╯
The API documentation lets us know how to issue a refresh request. The
differences are using grant_type=refresh_token and providing
refresh_token=<token> instead of login details.
Let's look at a request for unlocking one of the gym doors.
let headers = { authorization: 'Bearer t3t4noi1ppikvzfpctiymaxxwdw03xor2uhawmu0a1e5qld53j' }
let body = { lat: '74.01151' lng: '165.58725' qr: 'IQ4aP5xLB0Dn36BKmcWz' }
let url = 'https://<gym>.api.twiik.me/v3/<gym>/user/checkin'
http post --content-type application/x-www-form-urlencoded --headers $headers $url $body
This request is not in the API documentation. Interestingly, it uses the GPS location of the device to ensure we're not too far from where the QR code is supposed to be. I'm curious if anyone has ever triggered this error response except for me. Great reason to ask for location permissions, right?
╭─────────┬───────────────────────────────────────────────────────────────────────────────╮
│ code │ DISTANCE_ERROR │
│ title │ Bad request │
│ message │ Det gick tyvärr inte att öppna dörren, din position är för långt från gymmet. │
╰─────────┴───────────────────────────────────────────────────────────────────────────────╯
If the request is successful, we instead get the following response:
╭────────────┬──────╮
│ status │ ok │
│ checked_in │ true │
╰────────────┴──────╯
and the door hopefully unlocks (there have been technical issues preventing access). The request is repeated for the second door, except using the identifier from the second QR code. Note the first door has to be closed in order for the second one to be unlocked.
We now know how to login, refresh the access token, and unlock the doors. Maybe I just have too high expectations, but the iOS application is slow, and the whole procedure of scanning QR codes is annoying. Plus, I'd rather not provide location permissions. Wouldn't it be great if we were able to bypass the application entirely?
I had not used Apple Shortcuts extensively before, so it was a fun challenge to see if it was possible to unlock the doors with a shortcut. This could mean a single tap (on the Apple Watch) per door to perform the unlock (not counting taps to get to the Shortcuts application).
For the initial login, we need to perform a fairly standard POST request with an empty JSON body. Apple Shortcuts supports this natively and you can add both headers and body when necessary.
We need to store the response (at least the access token, the refresh token, and expiry time) somewhere and then avoid this login request if there's already a stored response. Shortcuts supports using Files (iCloud Drive) for storing and retrieving files.
The "Get dictionary from" step is parsing the JSON response into a dictionary, so that values can be retrieved by Shortcuts. If we would like to avoid storing our email and password in the shortcut, we can run this once and then change to only read the file from that point on.
There's a function Current Date to get the current time. But
we need an epoch value to compare with the expiry time. Turns out, you can
add a number to the returned date and it is converted to epoch seconds. In
the example below, I add 24 hours (86 400 seconds) to refresh
slightly early.
If we need to refresh the token, we run the refresh request and save the response by overwriting the existing file. If not, we can use the dictionary we already have from the first step.
The Shortcuts application supports
application/x-www-form-urlencoded requests. Instead of
providing our real location, we can use some fixed GPS coordinates close
to the gym. This enables remote unlocking of the doors, but the gym was
(surely?) aware of that possibility. Guessing this was only ever meant to
prevent taking pictures of the QR codes and scanning them at home.
We're now able to completely get rid of the application provided by the gym, and can use two (one per door) shortcuts to unlock the gym doors. Following is how the shortcuts turned out on macOS.
The shortcuts also automatically synchronize to iOS with the same appearance.
It's especially convenient to run the shortcuts using an Apple Watch.
The name of the gym and all identifying information has been randomized in the blog post. I hope you found the post useful and that it can inspire similar automations. While it's mostly unnecessary security to require location permissions for unlocking doors (not to mention privacy concerns), the remote unlocking possibility should not be abused. I've personally found no practical use for it.