-
Notifications
You must be signed in to change notification settings - Fork 2
Expand file tree
/
Copy pathview-token
More file actions
executable file
·379 lines (331 loc) · 12.4 KB
/
view-token
File metadata and controls
executable file
·379 lines (331 loc) · 12.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
#!/usr/bin/env bash
# view-token
#
# This script can decode and show macaroons and OIDC (JWT) tokens.
usage() {
cat <<-EOF
view-token - a script to view the properties of macaroons and OIDC tokens.
Arguments:
<filename> - file containing a token. Can be a plain text file with the bare token,
or an Rclone config file. In that case, view-token will search for
a line containing "bearer_token" and decode the value.
You can use a trick like this:
view-token <(cat <<<"\$my_token")
This allows you to read from a variable without exposing the variable
to other users with the ps command.
<no filename> - view-token will look for environment variable BEARER_TOKEN
and decode its value.
--minimal - Show only minimal information.
--help - Show this help text.
EOF
}
validate_expiration_timestamp () {
local exp_unix="$1" # Expiration timestamp (unix format)
local min_valid_time=60 # Token should be valid for more than this many seconds
# Do we actually have an expiration timestamp?
if [ -z "$exp_unix" ] || ! [[ "$exp_unix" =~ ^[0-9]+$ ]]; then
echo 1>&2 "ERROR: Invalid token: missing or invalid expiration field"
return 1
fi
# Get the current time in seconds since epoch
now=$(date +%s)
# Check if the token is expired
if [ "$now" -ge "$exp_unix" ]; then
echo 1>&2 "Token has expired $(( now - exp_unix )) seconds ago."
return 1
fi
# Check if the token is about to expire
if [ "$now" -ge "$(( exp_unix - min_valid_time ))" ]; then
echo 1>&2 "Warning: Token will expire in $(( exp_unix - now )) seconds."
return 1
fi
# If we get here, the expiration timestamp should be valid.
return 0
}
get_my_ip () {
case $1 in
4 )
curl --silent -4 https://ifconfig.co \
|| curl --silent -4 https://api.ipify.org \
|| curl --silent -4 https://checkip.amazonaws.com
;;
6 )
curl --silent -6 https://ifconfig.co \
|| curl --silent -6 https://api64.ipify.org \
|| curl --silent -6 https://v6.ident.me
;;
'' )
echo "ERROR: get_my_ip requires an IP version (4 or 6)."
exit 1
;;
* )
echo "ERROR: requested IP version is '$1' but it should be 4 or 6."
exit 1
;;
esac
}
validate_ip_caveat () {
# This function checks whether the IP address(es) of the local host
# match the macaroon's IP caveats.
#
# We need ipcalc for this. If it's not available, quit.
if [ ! -x "$(command -v ipcalc)" ]; then
echo 1>&2 "If you install ipcalc, I can check the IP caveat of your macaroon."
echo 1>&2 "You can install it with 'brew install ipcalc' (MacOS) or 'dnf install ipcalc' (RedHat and similar)"
return 1
fi
# The IP caveat from the macaroon
ip_caveat="$1"
if [ -z "$ip_caveat" ] ; then
echo 1>&2 "IP caveat is empty." \
"It is recommended to use IP caveats in a macaroon, to avoid theft and abuse."
return 1
fi
# Get the external addreses of the local system.
my_ipv4=$(get_my_ip 4)
my_ipv6=$(get_my_ip 6)
if [[ -z "$my_ipv4" && -z "$my_ipv6" ]]; then
echo "Couldn't find IP addresses on this system; can't check the IP caveat."
return 1
fi
# Temporarily set Internal Field Separator to comma to split the IP caveat
# (This does not affect awk)
OLDIFS="$IFS"
IFS=','
# Does the local system have an IPv4 address? Then check if it matches the macaroon.
if [ -n "$my_ipv4" ] ; then
# No match until we find one
match_ipv4=false
# Iterate over the values
for ip_caveat_item in $ip_caveat; do
# Check whether the networks of the IP caveat and the local address match.
# We take the subnet of the caveat and apply it to the local address.
# If ipcalc returns the same network for the IP caveat item and the the address
# of this system, it means there's a match.
# ipcalc on MacOS (darwin) is different from ipcalc in Linux, hence the OSTYPE check.
case $OSTYPE in
darwin* )
prefix=$(ipcalc --nobinary "$ip_caveat_item" | awk '/Netmask/{print $2}')
caveat_network=$(ipcalc --nobinary "$ip_caveat_item" | awk '/Prefix|Network/{print $2}')
my_ipv4_network=$(ipcalc --nobinary "$my_ipv4/$prefix" | awk '/Prefix|Network/{print $2}')
;;
* )
prefix=$(ipcalc --prefix "$ip_caveat_item" | sed -e 's/PREFIX=//')
if [ "$prefix" -gt 32 ] ; then
# This prefix must be IPv6 but here we're checking an IPv4 address. Skipping.
continue
fi
caveat_network=$(ipcalc --network "$ip_caveat_item" | sed -e 's/NETWORK=//')
my_ipv4_network=$(ipcalc --network --silent "$my_ipv4/$prefix" | sed -e 's/NETWORK=//')
;;
esac
if [[ "$caveat_network" == "$my_ipv4_network" ]] ; then
#echo "IPv4 address $my_ipv4 matches caveat subnet $ip_caveat_item."
match_ipv4=true
fi
done
fi # End if -n my_ipv4
# Does the local system have an IPv6 address? Then check if it matches the macaroon.
if [ -n "$my_ipv6" ] ; then
# No match until we find one
match_ipv6=false
# Iterate over the values
for ip_caveat_item in $ip_caveat; do
# Check whether the networks of the IP caveat and the local address match.
# We take the subnet of the caveat and apply it to the local address.
# If ipcalc returns the same network for the IP caveat item and the the address
# of this system, it means there's a match.
# ipcalc on MacOS (darwin) is different from ipcalc in Linux, hence the OSTYPE check.
case $OSTYPE in
darwin* )
netmask=$(ipcalc --nobinary "$ip_caveat_item" | awk '/Netmask/{print $2}')
caveat_network=$(ipcalc --nobinary "$ip_caveat_item" | awk '/Prefix|Network/{print $2}')
my_ipv6_network=$(ipcalc --nobinary "$my_ipv6/$netmask" | awk '/Prefix|Network/{print $2}')
;;
* )
netmask=$(ipcalc --netmask "$ip_caveat_item" | sed -e 's/NETMASK=//')
caveat_network=$(ipcalc --network "$ip_caveat_item" | sed -e 's/NETWORK=//')
my_ipv6_network=$(ipcalc --network "$my_ipv6/$netmask" | sed -e 's/NETWORK=//')
;;
esac
if [[ "$caveat_network" == "$my_ipv6_network" ]] ; then
#echo "IPv6 address $my_ipv6 matches caveat subnet $ip_caveat_item."
match_ipv6=true
fi
done
fi # End if -n my_ipv6
# Reset IFS to default (space, tab, newline)
IFS="$OLDIFS"
# Let's see what we got. Does IPv4 match? Does IPv6 match?
if [ -n "$my_ipv4" ] ; then
if $match_ipv4 ; then
if [ -n "$my_ipv6" ] ; then
if $match_ipv6 ; then
echo "Both your IPv4 ($my_ipv4) and IPv6 ($my_ipv6) match. This macaroon is valid from this system."
return 0
else
echo "Your IPv4 ($my_ipv4) matches, but your IPv6 ($my_ipv6) doesn't. Not sure if this macaroon will work on this system."
return 1
fi
else
# There is no IPv6 on this system
echo "Your IPv4 ($my_ipv4) matches. This macaroon is valid from this system."
return 0
fi
else
# IPv4 doesn't match
if [ -n "$my_ipv6" ] ; then
if $match_ipv6 ; then
echo "Your IPv6 ($my_ipv6) matches, but your IPv4 ($my_ipv4) doesn't. Not sure if this macaroon will work on this system."
else
echo "Neither your IPv4 ($my_ipv4) nor your IPv6 ($my_ipv6) address matches the IP caveat. This macaroon will not work on this system."
fi
else
echo "Your IPv4 ($my_ipv4) does not match. This macaroon will not work on this system."
fi
return 1
fi
else
# No IPv4 on this system.
if [ -n "$my_ipv6" ] ; then
if $match_ipv6 ; then
echo "Your IPv6 ($my_ipv6) matches. This macaroon is valid on this system."
return 0
else
echo "Your IPv6 ($my_ipv6) does not match. This macaroon will not work on this system."
return 1
fi
else
echo "No IP addresses found on this system. Could not validate IP caveat of macaroon."
return 1
fi
fi
}
check_token() {
local token="$1"
local token_debug_info="$2"
# Determine token type (JWT or Macaroon)
if [[ "$token" =~ ^[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+$ ]]; then
# JWT / OIDC token
payload=$(echo "$token" | cut -d "." -f2 | base64 -d 2>/dev/null | tr -d $'\n')
# Check if decoding succeeded
if [ -z "$payload" ]; then
echo 1>&2 "ERROR: Invalid token: cannot decode payload"
return 1
fi
# Show JWT / OIDC token
echo "$payload" | jq '. | .exp |= todate | .nbf |= todate | .iat |= todate'
# Extract expiration time (exp) from payload
exp_unix=$(echo "$payload" | jq -r '.exp' 2>/dev/null)
# Fail if the token has expired
if ! validate_expiration_timestamp "$exp_unix" "$token_debug_info" ; then
echo 1>&2 "JWT/OIDC token is no longer valid."
return 1
fi
# If we get here, it means we have a valid OIDC token.
$verbose && echo "JWT / OIDC token is still valid"
return 0
else
# Macaroon Token (assume it's base64 encoded)
# We use grep with --text to avoid a "Binary file" message.
# We use tr -d '\0' to strip off NUL characters,
# and 2>/dev/null to hide an error message on MacOS.
macaroon_decoded=$(echo "$token" \
| base64 -d 2>/dev/null \
| awk '{print substr($0, 5)}' 2>/dev/null \
| grep --text -v 'signature' \
| tr -d '\0' 2>/dev/null )
# Check if decoding succeeded
if [ -z "$macaroon_decoded" ]; then
echo 1>&2 "ERROR: Invalid token: cannot decode"
return 1
fi
# Show the macaroon
if $verbose ; then
echo -e "\033[34m\n$macaroon_decoded\n\033[0m" | sed -e 's/^/ /'
else
echo -e "\033[34m$macaroon_decoded\033[0m" | sed -e 's/^/ /'
fi
# Extract expiration time (before) using regex
exp=$(echo "$macaroon_decoded" | grep -o 'before:[0-9T:\.-]*Z' | sed -e 's/before://')
# Validate expiration field
if [ -z "$exp" ]; then
echo 1>&2 "ERROR: invalid macaroon: missing 'before' field"
return 1
fi
# Convert expiration time to unix time (seconds since epoch)
case $OSTYPE in
darwin* ) exp_unix=$(date -u -j -f "%Y-%m-%dT%H:%M:%S" "${exp:0:19}" +"%s" 2>/dev/null) ;;
* ) exp_unix=$(date -d "$exp" +%s 2>/dev/null) ;;
esac
if [ -z "$exp_unix" ]; then
echo 1>&2 "ERROR: invalid macaroon: unable to parse 'before' timestamp"
return 1
fi
# Fail if the token has (almost) expired
if ! validate_expiration_timestamp "$exp_unix" "$token_debug_info" ; then
echo 1>&2 "Macaroon is no longer valid."
return 1
fi
if $verbose ; then
# Validate IP caveat
ip_caveat=$(echo "$macaroon_decoded" | grep -o 'ip:.*' | sed -e 's/ip://')
validate_ip_caveat "$ip_caveat"
fi
# If we get here, it means we have a valid macaroon.
$verbose && echo "Macaroon is still valid"
return 0
fi
}
# Read command line arguments.
tokenfile=''
verbose=true
while [ $# -gt 0 ] ; do
# Argument can either be '--minimal' or a file name.
case "$1" in
--minimal )
verbose=false
shift
;;
--help )
usage
exit 1
;;
* )
tokenfile="$1"
shift
;;
esac
done
if [ -n "$tokenfile" ] ; then
$verbose && echo "Token source: $tokenfile"
#
# Read the tokenfile only once (It might be a file descriptor that will be closed after reading)
tokenfile_contents=$(<"$tokenfile") || {
echo "ERROR: unable to read token from '$tokenfile'" 1>&2
exit 1
}
#
# First, we assume the tokenfile is an Rclone config file.
token=$(sed -n 's/^bearer_token *= *//p' <<<"$tokenfile_contents")
if [ "$(wc -l <<<"$token")" -gt 1 ] ; then
echo 1>&2 "ERROR: file '$tokenfile' contains multiple tokens. Can't determine which one to show."
exit 1
fi
# If it was not an rclone config file, it may be a
# plain text file with only the token.
if [ -z "$token" ] ; then
token=$(head -n 1 <<<"$tokenfile_contents")
fi
else
# No tokenfile specified. Let's try BEARER_TOKEN.
if [ -n "$BEARER_TOKEN" ] ; then
token="$BEARER_TOKEN"
$verbose && echo "Token source: BEARER_TOKEN variable"
else
echo 1>&2 "No tokenfile specified, and BEARER_TOKEN is empty. Nothing to do."
exit 1
fi
# read BEARER_TOKEN
fi
check_token "$token" || exit 1