Authenticating Remote Connections

To establish a secure connection to a remote computer your connection must be authenticated. Authentication is the process of proving who your connection represents.

Exactly how your connection is authenticated can vary from remote computer to remote computer. This variation occurs because different computers can use different systems. Most Macs will authenticate against an OpenDirectory or Kerberos like service. However, thanks to Power Manager's support for Pluggable Authentication Modules (PAM) a wide range of alternative methods maybe used.

Power Manager's API provides a PMAuthenticator interface to deal with authentication. The interface also provides built-in authenticators to deal with the most common situations; in most cases you can delegate the task of authenticating to the provided code.

Two built-in authenticators deal with the two most common situations:

Passive: Username and Password

The authenticator returned by PMAuthenticatorCreateWithUsernameAndPassword will attempt to handle the authentication process with no interaction. Requests for a username or password are responded to using the values provided on creation.

This authenticator is suitable for graphical tools that can gather username and password information before creating a connection. This authenticator is also suitable for connections where the username and password can be fetched from a Keychain.

Interactive: Command Line Tool

The authenticator returned by PMAuthenticatorCreateForCommandLineTool passes all authentication requests, informational messages, and errors to the command line. The user can then read and respond to the requests as required.

This authenticator is suitable for command line tools that have interactive terminals. This authenticator mirrors the PAM conversation implementation and allows complex PAM modules full access to the user.

PMAuthenticatorCreate

The authenticator returned by PMAuthenticatorCreate is provided for situations where you want absolute control over the authentication process.

Providing NULL

If you do not provide a PMAuthenticator instance, then your connection will be restricted to local connections. Local connections do not require authentication. Instead, local connections use authorisation rights to determine what your connection can and can not do.

CFAuthenticator: Using an authenticator

To demonstrate how to use a PMAuthenticator instance, we are going to build a small command line tool in C. The code for this example is available in the Power Manager/Developer/Examples folder.

Use XCode to create a new Core Services command line application.

Add the PowerManager.framework to your project. Do this by dragging the PowerManager.framework folder from the /Library/Frameworks folder onto your project. Alternatively, use the Add to Project menu item.

We are going to add our code to the main.c file. XCode 3.2's default main file contains the following:

Example 1.3. Default main.c of a Core Services XCode 3.2 project

#include <CoreServices/CoreServices.h>

int main (int argc, const char * argv[]) {
    // insert code here...
    printf("Hello, World!\n");
    return 0;
}


We need to add the Power Manager headers to allow calls to the appropriate functions. Do this by adding an include statement:

Example 1.4. Include the PowerManager framework headers.

#include <CoreServices/CoreServices.h>
#include <PowerManager/PowerManager.h>

int main (int argc, const char * argv[]) {
    // insert code here...
    printf("Hello, World!\n");
    return 0;
}

Next we create a PMConnectionRef instance using PMConnectionCreate. The returned instance represents our connection.

I have added a PMConnectionRelease call to the base of the main function. I find it helpful to add the balancing release when I write the create call. This stops me needing to track what resources need releasing later on. It is important to balance create and copy calls with release calls in the Power Manager API. If you do not release items you have created or copied, memory and resources may be leaked until the your process exits.

Example 1.5. Create a PMConnectionRef.

#include <CoreServices/CoreServices.h>
#include <PowerManager/PowerManager.h>

int main (int argc, const char * argv[]) {
    // insert code here...
    printf("Hello, World!\n");
    
    // Create a new connection using reasonable defaults
    PMConnectionRef connection = PMConnectionCreate(kCFAllocatorDefault,CFRunLoopGetMain(),kCFRunLoopCommonModes);
    
    // ...
    
    // Release the connection before exiting
    PMConnectionRelease(connection);
    
    return 0;
}

We are going to use the command line authenticator to handle authentication. This will let our command line tool support any authentication method.

Create a PMAuthenticatorRef using the PMAuthenticatorCreateForCommandLineTool function.

Pass the returned PMAuthenticatorRef to our connection. The connection will retain the authenticator and thus we immediately able to release the authenticator.

Release the authenticator using the PMAuthenticatorRelease function. While we have released our claim on the authenticator, the connection has not. This means the connection is now responsible for the authenticator. When we release the connection, the connection will in turn release it the authenticator.

Example 1.6. Create and set a PMAuthenticator.

#include <CoreServices/CoreServices.h>
#include <PowerManager/PowerManager.h>

int main (int argc, const char * argv[]) {
    // insert code here...
    printf("Hello, World!\n");
    
    // Create a new connection using reasonable defaults
    PMConnectionRef connection = PMConnectionCreate(kCFAllocatorDefault,CFRunLoopGetMain(),kCFRunLoopCommonModes);
    
    // Create an authenticator instance
    PMAuthenticatorRef authenticator = PMAuthenticatorCreateForCommandLineTool(kCFAllocatorDefault);
    
    // Associate the authenticator with the connection
    PMConnectionSetAuthenticator(connection,authenticator);
    
    // Release the authenticator; the connection retains on set
    PMAuthenticatorRelease(authenticator);
    
    // ...
    
    // Release the connection before exiting
    PMConnectionRelease(connection);
    
    return 0;
}

Your connection is now ready to connect. If the connection requires authentication, the command line authenticator will handle the requests for you.

Lets add the code needed to connect to a remote connection, and enter the runloop.

Example 1.7. Connect to a URL and enter the runloop.

#include <CoreServices/CoreServices.h>
#include <PowerManager/PowerManager.h>

int main (int argc, const char * argv[]) {
    // insert code here...
    printf("Hello, World!\n");
    
    // Create a new connection using reasonable defaults
    PMConnectionRef connection = PMConnectionCreate(kCFAllocatorDefault,CFRunLoopGetMain(),kCFRunLoopCommonModes);
    
    // Create an authenticator instance
    PMAuthenticatorRef authenticator = PMAuthenticatorCreateForCommandLineTool(kCFAllocatorDefault);
    
    // Associate the authenticator with the connection
    PMConnectionSetAuthenticator(connection,authenticator);
    
    // Release the authenticator; the connection retains on set
    PMAuthenticatorRelease(authenticator);
        
    // Create a URL from a hardcoded string
    CFURLRef url = CFURLCreateWithString(kCFAllocatorDefault,CFSTR("pm://mac-pro.local:56390"),NULL);
    
    // Begin connection to URL with no options
    bool validConnection = PMConnectionConnectToURL(connection,url,NULL);
    
    // Release the URL
    CFRelease(url);
    
    if (validConnection == true)
    {   
        // ...
        
        // Connection begun, enter the runloop
        CFRunLoopRun();
    }
        
    // Release the connection before exiting
    PMConnectionRelease(connection);
    
    return 0;
}

You will need to modify the hardcoded Power Manager URL before building and running the tool.

This tool creates a connection to a remote Power Manager instance and authenticates. If all goes well the tool is left with an authenticated secure connection to a remote instance of Power Manager.

When CFAuthenticator is run with a URL to a computer running Mac OS X 10.6 Client, we see the following interaction:

The two prompts Login: and Password: are being provided by the remote Power Manager instance. The built-in command line authenticator is passing those prompts through to the command line for the user to answer. Answers are packaged up and returned to the remote Power Manager instance. This back and forth happens over a connection encrypted by SSL; Power Manager transparently deals with the encryption for you.

After authenticating, or failing, the tool will idle in the runloop until you terminate the process with Command+Period (.).

To extend this example, add a notification observer to the connection to observe kPMConnectionNotificationDidAuthenticate and kPMConnectionNotificationDidBecomeInvalid notifications.

Writing your own authenticator

Power Manager's API offers the ability to provide your own authenticator. By taking on this role, your code must handle the conversation taking place between the connection and remote instance of Power Manager.

Where possible, use one of the built-in authenticators to authenticate. The built-in authenticators are well tested and flexible enough for most situations.

Your role in the authentication conversation is to handle the incoming requests. Each request must be handled in turn. There may be zero, one, or many requests.

Requests are provided as Core Foundation structures. Requests are passed in as a CFArrayRef containing CFDictionaryRefs.

Your responses must be returned as Core Foundation structures. Your response must be a CFArrayRef containing an equal number of items as requests. The items must be the constant kCFNull, or an allocated CFStringRef or CFDictionaryRef.

The content of the requests and responses depends on the form of the request. The form of a request is determined by a type value.

Requests contain a CFNumberRef keyed by the constant kPMAuthenticationRequestKeyType.

There are three types of request:

  • A prompt for information from the user;
  • An informative message to be displayed to the user;
  • An error message to be displayed to the user.

Prompts for Information

A prompt for more information has the type kPMAuthenticationRequestTypePrompt. A prompt is used to interact with the user. A prompt will be used to ask for a login name and password, or to request other security information from the user.

Do not record or log the contents of responses that have echo set to false.

Your authenticator must return a CFStringRef containing the response to the prompt. You may optionally embed your CFStringRef in a CFDictionaryRef using the key kPMAuthenticationRequestKeyMessage.

Table 1.1. CFAuthenticator's kPMAuthenticationRequestTypePrompt request type.

Key Type Description
kPMAuthenticationRequestKeyType CFNumberRef

A numeric value equal to kPMAuthenticationRequestTypePrompt

kPMAuthenticationRequestKeyMessage CFStringRef

A textual prompt to display to the user. A textual response is required from the user.

kPMAuthenticationRequestKeyEcho CFBooleanRef

Should the user's response be echoed back to them. If false, do not display the user's key strokes; the request is for confidential information such as a password.

If this value is not present, assume echo is true.


Informative Messages

An informative message has the type kPMAuthenticationRequestTypeMessage. Informative messages are used to pass information back to the user.

No user response is expected. Your authenticator must return a kCFNull, empty CFStringRef, or empty CFDictionaryRef, to indicate the informative message has been passed on to the user.

Table 1.2. CFAuthenticator's kPMAuthenticationRequestTypeMessage request type.

Key Type Description
kPMAuthenticationRequestKeyType CFNumberRef

A numeric value equal to kPMAuthenticationRequestTypeMessage

kPMAuthenticationRequestKeyMessage CFStringRef

A textual message to display to the user.


Error Messages

An error message has the type kPMAuthenticationRequestTypeError. Error messages are used to pass errors and warnings back to the user.

No user response is expected. Your authenticator must return a kCFNull, empty CFStringRef, or empty CFDictionaryRef, to indicate the error message has been passed on to the user.

Table 1.3. CFAuthenticator's kPMAuthenticationRequestTypeMessage request type.

Key Type Description
kPMAuthenticationRequestKeyType CFNumberRef

A numeric value equal to kPMAuthenticationRequestTypeError

kPMAuthenticationRequestKeyMessage CFStringRef

A textual message to display to the user.


CFCustomAuthenticator: Creating an authenticator

To demonstrate how to create a custom PMAuthenticatorRef instance, we are going to build a small command line tool in C. The code for this example is available in the Power Manager/Developer/Examples folder.

Use XCode to create a new Core Services command line application.

Add the PowerManager.framework to your project. Do this by dragging the PowerManager.framework folder from the /Library/Frameworks folder onto your project. Alternatively, use the Add to Project menu item.

We are going to add all our code to the main.c file. For larger projects you should consider separating out your authenticator code into a separate file.

The first task is to create a PMAuthenticatorRef using the PMAuthenticatorCreate function. This function uses a context structure to determine how to respond to authentication requests.

The context structure is copied by PMAuthenticatorCreate, so it can be allocated on the stack. After calling PMAuthenticatorCreate, you can dispose of your copy of the context structure.

PMAuthenticator requires a single callback. The context structure also includes fields for a reference counted item of your choosing. This item is optional, and if provided, will be passed to the authenticator's callbacks. If no item is provided, NULL is provided to the callbacks.

At this stage the authenticator callback is an empty function with a prototype. The code will compile but connections will not authenticate.

Example 1.8. Create and set a custom PMAuthenticator.

#include <CoreServices/CoreServices.h>
#include <PowerManager/PowerManager.h>

// Authenticator callback
static CFArrayRef MyAuthenticatorDidRequest(PMAuthenticatorRef inAuthenticator,CFArrayRef inRequests,void* inInfo);

#pragma mark -

int main (int argc, const char * argv[]) {

    // Set up the authenticator context
    PMAuthenticatorContext context;
    memset(&context,0,sizeof(PMAuthenticatorContext));
    context.didRequest = MyAuthenticatorDidRequest;
    
    // Create the authenticator
    PMAuthenticatorRef myAuthenticator = PMAuthenticatorCreate(kCFAllocatorDefault,&context);
    assert(myAuthenticator != NULL);
    
    // The context structure is no longer needed
    
    // ...create a connection and associate the authenticator
    PMConnectionRef myConnection = PMConnectionCreate(kCFAllocatorDefault,CFRunLoopGetMain(),kCFRunLoopCommonModes);
    assert(myConnection != NULL);
    
    // Associate authenticator with the connection
    PMConnectionSetAuthenticator(myConnection,myAuthenticator);
    
    // Release the authenticator; the connection retained as needed
    PMAuthenticatorRelease(myAuthenticator);
    
    // ...connect, enter runloop, and handle responses
    
    // Release the connection after runloop
    PMConnectionRelease(myConnection);
    
    return EXIT_SUCCESS;
}

// Authenticator callback
static CFArrayRef MyAuthenticatorDidRequest(PMAuthenticatorRef inAuthenticator,CFArrayRef inRequests,void* inInfo)
{
    // TODO: Add request handling loop
    
    return NULL;
}

The next step is to write the authenticator's callback. The callback must return an allocated CFArrayRef. Create a new mutable array for the responses.

Next iterate over every request calling out to a separation function to deal with the individual requests.

Example 1.9. Create a response array and iterate over the requests.

// ...[includes]

// Authenticator callback
static CFArrayRef MyAuthenticatorDidRequest(PMAuthenticatorRef inAuthenticator,CFArrayRef inRequests,void* inInfo);

// Authenticator callback for a single request (CFArrayApplier)
static void MyAuthenticatorAppendResponseApplier(const void* inRequest,void* inResponses);

// ...[main function]

// Authenticator callback
static CFArrayRef MyAuthenticatorDidRequest(PMAuthenticatorRef inAuthenticator,CFArrayRef inRequests,void* inInfo)
{
    assert(inAuthenticator != NULL);
    assert(inRequests != NULL);
    assert(inInfo == NULL);

    // Create an array to contain the responses
    CFMutableArrayRef myResponses = CFArrayCreateMutable(kCFAllocatorDefault,0,&kCFTypeArrayCallBacks);
    assert(myResponses != NULL);
    
    // Iterate over the requests, adding responses for each
    CFIndex requestCount = CFArrayGetCount(inRequests);
    CFRange everyRequest = CFRangeMake(0,requestCount);
    CFArrayApplyFunction(inRequests,everyRequest,MyAuthenticatorAppendResponseApplier,myResponses);
    
    // Did we append the right number of responses
    assert(requestCount == CFArrayGetCount(myResponses));
    
    return myResponses;
}

// Authenticator callback for a single request (CFArrayApplier)
static void MyAuthenticatorAppendResponseApplier(const void* inRequest,void* inResponses)
{
    // TODO: Append a response for this request
}

[Note] Note

If your authenticator is designed for a graphical interface, you may like to group the handling of your requests.

Consider constructing an interface in one step, presenting the interface and gathering the responses in the next step, and finally creating a response array for the last step.

The implementation of the request handling will be specific to your needs. For this example we return constants. The returned array of responses is unlikely to result in an authenticated connection, but the responses are appropriately formed.

In the remaining code are TODO and FIXME tags to highlight the specific regions that you need to implement.

Example 1.10. Get the request type for request.

// ...[includes]

// ...[prototypes]

// ...[main function]

// ...[MyAuthenticatorDidRequest function]

// Authenticator callback for a single request (CFArrayApplier)
static void MyAuthenticatorAppendResponseApplier(const void* inRequest,void* inResponses)
{
    assert(inRequest != NULL);
    assert(inResponses != NULL);
    
    // What type of request is this?
    CFNumberRef requestType = CFDictionaryGetValue(inRequest,kPMAuthenticationRequestKeyType);
    assert(requestType != NULL);
    // ...
    SInt32 requestTypeValue;
    bool validRequestTypeValue = CFNumberGetValue(requestType,kCFNumberSInt32Type,&requestTypeValue);
    assert(validRequestTypeValue == true);
    
    // Get the message, if it exists
    CFStringRef message = CFDictionaryGetValue(inRequest,kPMAuthenticationRequestKeyMessage);
    
    switch (requestTypeValue)
    {
        // TODO: Handle each request type
            
        // Unknown type
        case kPMAuthenticationRequestTypeUnsupported:
        default:
        {
            // Append NULL to response; authentication failure is now likely
            CFArrayAppendValue(inResponses,kCFNull);
            break;
        }
    }
}


The prompt request type is likely to be your main focus. For this example we return a constant CFStringRef.

Example 1.11. Handle the prompt request type.

        case kPMAuthenticationRequestTypePrompt:
        {
            // Message must exist
            assert(message != NULL);
            
            // Get the echo state
            bool echoValue = true;
            CFBooleanRef echo = CFDictionaryGetValue(inRequest,kPMAuthenticationRequestKeyEcho);
            if (echo != NULL)
            {
                echoValue = CFBooleanGetValue(echo);
            }
            
            // TODO: display the message to the user as a question
            // TODO: get a response
            // FIXME: respond with the real response
            CFArrayAppendValue(inResponses,CFSTR("hello world"));
            
            break;
        }


The informative message request type is easy to respond with kCFNull.

Example 1.12. Handle the informative message request type.

        case kPMAuthenticationRequestTypeMessage:
        {
            // Message must exist
            assert(message != NULL);
            
            // TODO: display the informative message to the user
            
            // Respond with a constant to acknowledge message
            CFArrayAppendValue(inResponses,kCFNull);
            break;
        }


The error message request type is also easy to respond with kCFNull. Do not assume an error message request is fatal or ensures your authenticator will fail to authenticate. Authenticator conversations can be complex and involve resetting passwords, or completing challenges that allow for one or more failed attempts.

Example 1.13. Handle the error message request type.

        case kPMAuthenticationRequestTypeError:
        {
            // Message must exist
            assert(message != NULL);
            
            // TODO: display the error message to the user
            
            // Respond with a constant to acknowledge error
            CFArrayAppendValue(inResponses,kCFNull);
            break;
        }


Writing a custom authenticator is not difficult. In most situations you should never need to implement one yourself, but the possibility exists.