It’s always hard to nail the exact sequence when authorizing through a new API. Here’s what I came up with for PHP authorization with Zoom’s JWTs. This is essentially a quick start which gets you enough functions to do a first API call: to list zoom users. Line 40->68 is where the JWt meat happens.
<?php // config parameters you need to define define( 'ZOOM_TOKEN_FILE', "/var/.zoom_token" ) ; // some location on the filesystem used to cache your token while it's current (make sure permissions are restrictive) define( 'ZOOM_API_KEY', "" ) ; define( 'ZOOM_API_SECRET', "" ) ; // main print_r( zoom_list_users() ) ; exit( 0 ) ; // functions function zoom_list_users() { $users = array() ; $page_number = 1 ; $keep_going = true ; while( $keep_going && $page_number<10000 ) { $result = zoom_make_api_call( "GET", "https://api.zoom.us/v2/users", array('page_size'=>300, 'page_number'=>$page_number, 'status'=>"active") ) ; $result = json_decode( $result, true ) ; if( array_key_exists('users', $result) && count($result['users'])>0 ) { foreach( $result['users'] as $user ) { $users[] = $user ; } $page_number++ ; if( $page_number>$result['page_count'] ) { $keep_going = false ; } } else { $keep_going = false ; } } return $users ; } // PHP's default base64 encode isn't URL safe which messes up the JWT, we need these functions instead function base64_url_encode( $data ) { return rtrim( strtr(base64_encode($data), '+/', '-_'), '=' ) ; } function base64_url_decode( $data ) { return base64_decode( str_pad(strtr($data, '-_', '+/'), strlen($data) % 4, '=', STR_PAD_RIGHT) ) ; } function get_token( $refresh=false ) { if( $refresh===false && file_exists(ZOOM_TOKEN_FILE) ) { return file_get_contents( ZOOM_TOKEN_FILE ) ; } $jwt_request_date = @date( "U" ) ; // no warning, proper system timezone assumed $jwt_expiration_date = $jwt_request_date + 60*60 ; # +1 hour $jwt_header = '{"alg":"HS256","typ":"JWT"}' ; $jwt_claim_set = '{"iss":"' . ZOOM_API_KEY . '","exp":' . $jwt_expiration_date . '}' ; $jwt_signature = sign_data( base64_url_encode($jwt_header) . '.' . base64_url_encode($jwt_claim_set), ZOOM_API_SECRET ) ; $jwt = base64_url_encode( $jwt_header ) . "." . base64_url_encode( $jwt_claim_set ) . "." . base64_url_encode( $jwt_signature ) ; file_put_contents( ZOOM_TOKEN_FILE, $jwt ) ; return $jwt ; } function sign_data( $data, $key ) { return hash_hmac( "SHA256" , $data, $key, true) ; } function zoom_make_api_call( $request, $url, $get_variables=null, $post_variables=null, $force_refresh_token=false ) { $ch = curl_init() ; $token = get_token( $force_refresh_token ) ; $getfields = "" ; if( $get_variables!==null && is_array($get_variables) ) { foreach( $get_variables as $get_variable_key=>$get_variable_value ) { $getfields .= "&" . urlencode( $get_variable_key ) . "=" . urlencode( $get_variable_value ) ; } if( strlen($getfields)>0 ) { $getfields = "?" . substr( $getfields, 1 ) ; } } curl_setopt( $ch, CURLOPT_URL, "{$url}{$getfields}" ) ; curl_setopt( $ch, CURLOPT_PORT , 443 ) ; curl_setopt( $ch, CURLOPT_CUSTOMREQUEST, $request ) ; curl_setopt( $ch, CURLOPT_SSL_VERIFYPEER, false ) ; if( $post_variables!==null && is_array($post_variables) ) { $postfields = "" ; foreach( $post_variables as $post_variable_key=>$post_variable_value ) { $postfields .= "&" . urlencode( $post_variable_key ) . "=" . urlencode( $post_variable_value ) ; } $postfields = substr( $postfields, 1 ) ; curl_setopt( $ch, CURLOPT_POSTFIELDS, $postfields ) ; } else if( $post_variables!==null && is_string($post_variables) ) { curl_setopt( $ch, CURLOPT_POSTFIELDS, $post_variables ) ; } curl_setopt( $ch, CURLOPT_HTTPHEADER, array( "authorization: Bearer {$token}", "content-type: application/json") ) ; curl_setopt( $ch, CURLOPT_RETURNTRANSFER, 1 ) ; curl_setopt( $ch, CURLOPT_HEADER, true ) ; $response = curl_exec( $ch ) ; curl_close( $ch ) ; $response = parse_http_response( $response ) ; if( $response['code']=="200" || $response['code']=="204" || $response['code']=="404" ) { return $response['body'] ; } else if( $response['code']=="401" ) { // expired token if( $force_refresh_token===false ) { // safe to recurse return zoom_make_api_call( $request, $url, $get_variables, $post_variables, true ) ; } else { echo "ERROR: had an expired token and I tried to refresh it, yet somehow it's still expired\n" ; print_r( $response ) ; exit( 1 ) ; } } else { echo "ERROR: I have no idea what to do with this response from Zoom\n" ; print_r( $response ) ; exit( 1 ) ; } } function parse_http_response( $raw_data ) { $parsed_response = array( 'code'=>-1, 'headers'=>array(), 'body'=>"" ) ; $raw_data = explode( "\r\n", $raw_data ) ; $parsed_response['code'] = explode( " ", $raw_data[0] ) ; $parsed_response['code'] = $parsed_response['code'][1] ; $i = 1 ; if( $parsed_response['code']=="100" ) { $parsed_response['code'] = explode( " ", $raw_data[2] ) ; $parsed_response['code'] = $parsed_response['code'][1] ; $i = 3 ; } for( ; $i<count($raw_data) ; $i++ ) { $raw_datum = $raw_data[$i] ; $raw_datum = trim( $raw_datum ) ; if( $raw_datum!="" ) { if( substr_count($raw_datum, ':')>=1 ) { $raw_datum = explode( ':', $raw_datum, 2 ) ; $parsed_response['headers'][strtolower($raw_datum[0])] = trim( $raw_datum[1] ) ; } else { echo "ERROR: we're in the headers section of parsing an HTTP section and no colon was found for line: {$raw_datum}\n" ; exit( 1 ) ; } } else { // we've moved to the body section if( ($i+1)<count($raw_data) ) { for( $j=($i+1) ; $j<count($raw_data) ; $j++ ) { $parsed_response['body'] .= $raw_data[$j] . "\n" ; } } // we don't need to continue the $i loop break ; } } return $parsed_response ; } ?>