Private
Public Access
1
0

Auth: adds JWT bearer auth via /authenticate.

Works in addition to digest auth
This commit is contained in:
James Magahern
2021-07-06 22:52:33 -07:00
parent f64ffcb8cc
commit 4d51ba7dd2
8 changed files with 314 additions and 10 deletions

View File

@@ -0,0 +1,26 @@
//
// MBIMAuthToken.h
// MBIMAuthToken
//
// Created by James Magahern on 7/6/21.
// Copyright © 2021 James Magahern. All rights reserved.
//
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface MBIMAuthToken : NSObject
@property (nonatomic, readonly) NSString *username;
@property (nonatomic, readonly) NSString *jwtToken;
@property (nonatomic, readonly) NSDate *expirationDate;
- (instancetype)initWithUsername:(NSString *)username NS_DESIGNATED_INITIALIZER;
- (instancetype)initWithTokenString:(NSString *)tokenString NS_DESIGNATED_INITIALIZER;
- (BOOL)isValid;
- (instancetype)init NS_UNAVAILABLE;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,149 @@
//
// MBIMAuthToken.m
// MBIMAuthToken
//
// Created by James Magahern on 7/6/21.
// Copyright © 2021 James Magahern. All rights reserved.
//
#import "MBIMAuthToken.h"
#import <CommonCrypto/CommonHMAC.h>
#define HOUR 3600
#define DAY (24*HOUR)
static const NSTimeInterval ExpirationTime = 15 * DAY;
static const char *SecretKey = "709E7CD8-4983-4D5F-B7BF-8B1C6341D2DB";
static NSString *const kUsernamePayloadKey = @"user";
static NSString *const kIssuerPayloadKey = @"iss";
static NSString *const kExpirationDatePayloadKey = @"exp";
static NSString *const kIssuerPayloadValue = @"kordophone";
@interface MBIMAuthToken ()
@property (nonatomic, copy) NSString *username;
@property (nonatomic, copy) NSString *jwtToken;
@property (nonatomic, copy) NSDate *expirationDate;
// JWT Payload data
@property (nonatomic, copy) NSString *headerString;
@property (nonatomic, copy) NSString *payloadString;
@property (nonatomic, copy) NSData *signatureData;
@end
@implementation MBIMAuthToken
- (instancetype)initWithUsername:(NSString *)username
{
self = [super init];
if (self) {
self.username = username;
self.expirationDate = [NSDate dateWithTimeIntervalSinceNow:ExpirationTime];
}
return self;
}
- (instancetype)initWithTokenString:(NSString *)tokenString
{
NSArray<NSString *> *components = [tokenString componentsSeparatedByString:@"."];
if (components.count != 3) {
return nil;
}
NSString *header = components[0];
NSString *payload = components[1];
NSString *signature = components[2];
NSData *payloadData = [[NSData alloc] initWithBase64EncodedString:payload options:0];
NSDictionary *decodedPayload = [NSJSONSerialization JSONObjectWithData:payloadData options:0 error:nil];
if (!decodedPayload) {
return nil;
}
if (![decodedPayload[kIssuerPayloadKey] isEqualToString:@"kordophone"]) {
return nil;
}
self = [super init];
if (self) {
_headerString = header;
_payloadString = payload;
_signatureData = [[NSData alloc] initWithBase64EncodedString:signature options:0];
_username = decodedPayload[kUsernamePayloadKey];
NSTimeInterval expirationDate = [decodedPayload[kExpirationDatePayloadKey] floatValue];
_expirationDate = [NSDate dateWithTimeIntervalSince1970:expirationDate];
}
return self;
}
- (NSUInteger)hash
{
return (_username.hash ^ _expirationDate.hash);
}
- (NSString *)jwtToken
{
if (!_jwtToken) {
NSDictionary *header = @{
@"alg" : @"HS256",
@"typ" : @"jwt"
};
NSData *headerData = [NSJSONSerialization dataWithJSONObject:header options:0 error:nil];
NSString *headerStr = [headerData base64EncodedStringWithOptions:0];
NSInteger expirationDate = [_expirationDate timeIntervalSince1970];
NSDictionary *payload = @{
kUsernamePayloadKey : _username,
kIssuerPayloadKey : kIssuerPayloadValue,
kExpirationDatePayloadKey : [NSString stringWithFormat:@"%ld", expirationDate]
};
NSData *payloadData = [NSJSONSerialization dataWithJSONObject:payload options:0 error:nil];
NSString *payloadStr = [payloadData base64EncodedStringWithOptions:0];
NSString *jwtDataStr = [NSString stringWithFormat:@"%@.%@", headerStr, payloadStr];
NSData *jwtData = [jwtDataStr dataUsingEncoding:NSASCIIStringEncoding];
unsigned char signature[CC_SHA256_DIGEST_LENGTH];
CCHmac(kCCHmacAlgSHA256, SecretKey, sizeof(SecretKey), jwtData.bytes, jwtData.length, signature);
NSData *signatureData = [NSData dataWithBytes:signature length:CC_SHA256_DIGEST_LENGTH];
NSString *signatureStr = [signatureData base64EncodedStringWithOptions:0];
_jwtToken = [NSString stringWithFormat:@"%@.%@", jwtDataStr, signatureStr];
}
return _jwtToken;
}
- (BOOL)isValid
{
// Verify expiration date
BOOL expirationDateValid = [_expirationDate timeIntervalSinceNow] > 0;
if (!expirationDateValid) {
MBIMLogInfo(@"Auth token expired.");
return NO;
}
// Verify signature
NSString *verificationDataStr = [NSString stringWithFormat:@"%@.%@", _headerString, _payloadString];
NSData *verificationData = [verificationDataStr dataUsingEncoding:NSASCIIStringEncoding];
unsigned char computedSignature[CC_SHA256_DIGEST_LENGTH];
CCHmac(kCCHmacAlgSHA256, SecretKey, sizeof(SecretKey), verificationData.bytes, verificationData.length, computedSignature);
NSData *computedSignatureData = [NSData dataWithBytes:computedSignature length:CC_SHA256_DIGEST_LENGTH];
if (![computedSignatureData isEqualToData:_signatureData]) {
MBIMLogInfo(@"Auth token signature verification failed.");
return NO;
}
return YES;
}
@end

View File

@@ -11,8 +11,14 @@
#import "MBIMBridge.h"
#import "MBIMBridge_Private.h"
#import "MBIMBridgeOperation.h"
#import "MBIMAuthToken.h"
#import <Security/Security.h>
#import <CocoaHTTPServer/HTTPMessage.h>
@interface HTTPConnection (/* INTERNAL */)
- (BOOL)isAuthenticated;
@end
@implementation MBIMHTTPConnection {
NSMutableData *_bodyData;
@@ -31,7 +37,15 @@
- (BOOL)isPasswordProtected:(NSString *)path
{
return [[MBIMBridge sharedInstance] usesAccessControl];
if ([[MBIMBridge sharedInstance] usesAccessControl]) {
NSURL *url = [NSURL URLWithString:path];
NSString *endpointName = [url lastPathComponent];
Class operationClass = [MBIMBridgeOperation operationClassForEndpointName:endpointName];
return [operationClass requiresAuthentication];
}
return NO;
}
- (NSString *)passwordForUser:(NSString *)username
@@ -41,7 +55,23 @@
return bridge.authPassword;
}
return @"";
// nil means "user not in system"
return nil;
}
- (BOOL)isAuthenticated
{
NSString *authInfo = [request headerField:@"Authorization"];
if ([authInfo hasPrefix:@"Bearer"]) {
NSArray *bearerAuthTuple = [authInfo componentsSeparatedByString:@" "];
if ([bearerAuthTuple count] == 2) {
NSString *jwtToken = [bearerAuthTuple objectAtIndex:1];
MBIMAuthToken *authToken = [[MBIMAuthToken alloc] initWithTokenString:jwtToken];
return [authToken isValid];
}
}
return [super isAuthenticated];
}
- (BOOL)useDigestAccessAuthentication

View File

@@ -0,0 +1,17 @@
//
// MBIMAuthenticateOperation.h
// MBIMAuthenticateOperation
//
// Created by James Magahern on 7/6/21.
// Copyright © 2021 James Magahern. All rights reserved.
//
#import "MBIMBridgeOperation.h"
NS_ASSUME_NONNULL_BEGIN
@interface MBIMAuthenticateOperation : MBIMBridgeOperation
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,72 @@
//
// MBIMAuthenticateOperation.m
// MBIMAuthenticateOperation
//
// Created by James Magahern on 7/6/21.
// Copyright © 2021 James Magahern. All rights reserved.
//
#import "MBIMAuthenticateOperation.h"
#import "MBIMBridge.h"
#import "MBIMAuthToken.h"
@implementation MBIMAuthenticateOperation
+ (void)load { [super load]; }
+ (NSString *)endpointName
{
return @"authenticate";
}
+ (BOOL)requiresAuthentication
{
return NO;
}
- (void)main
{
NSObject<HTTPResponse> *response = nil;
if (self.requestBodyData.length == 0) {
self.serverCompletionBlock([[HTTPErrorResponse alloc] initWithErrorCode:400]);
return;
}
NSError *error = nil;
NSDictionary *args = [NSJSONSerialization JSONObjectWithData:self.requestBodyData options:0 error:&error];
if (error || args.count == 0) {
response = [[HTTPErrorResponse alloc] initWithErrorCode:400];
} else {
do {
NSString *username = [args objectForKey:@"username"];
NSString *password = [args objectForKey:@"password"];
if (!username || !password) {
response = [[HTTPErrorResponse alloc] initWithErrorCode:400];
break;
}
if (![MBIMBridge.sharedInstance.authUsername isEqualToString:username]) {
response = [[HTTPErrorResponse alloc] initWithErrorCode:401];
break;
}
if (![MBIMBridge.sharedInstance.authPassword isEqualToString:password]) {
response = [[HTTPErrorResponse alloc] initWithErrorCode:401];
break;
}
MBIMAuthToken *token = [[MBIMAuthToken alloc] initWithUsername:username];
// All systems go
response = [MBIMJSONDataResponse responseWithJSONObject:@{
@"jwt" : token.jwtToken
}];
} while (NO);
}
self.serverCompletionBlock(response);
}
@end

View File

@@ -18,6 +18,7 @@ typedef void (^MBIMBridgeOperationCompletionBlock)(NSObject<HTTPResponse> * _Nul
@interface MBIMBridgeOperation : NSOperation
@property (class, nonatomic, readonly) NSString *endpointName;
@property (class, nonatomic, readonly) BOOL requiresAuthentication; // default YES
@property (nonatomic, strong) NSData *requestBodyData;
@property (nonatomic, readonly) NSURL *requestURL;

View File

@@ -55,6 +55,11 @@
return [[self _operationClassMapping] objectForKey:endpointName];
}
+ (BOOL)requiresAuthentication
{
return YES;
}
- (instancetype)initWithRequestURL:(NSURL *)requestURL completion:(MBIMBridgeOperationCompletionBlock)completionBlock
{
self = [super init];